From acbfdb8f4c375d46aab94ab9c1ce01095580d851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Wed, 16 Nov 2016 22:35:33 +0100 Subject: [PATCH 01/11] update to Elasticearch 5.0.1 --- README.adoc | 2 +- build.gradle | 11 +- .../java/org/elasticsearch/node/MockNode.java | 9 +- .../org/xbib/elasticsearch/AliasTest.java | 26 +- .../{NodeTestUtils.java => NodeTestBase.java} | 148 +++++---- .../org/xbib/elasticsearch/SearchTest.java | 11 +- .../org/xbib/elasticsearch/SimpleTest.java | 29 +- .../org/xbib/elasticsearch/WildcardTest.java | 18 +- .../client/node/BulkNodeClientTest.java | 34 +- .../client/node/BulkNodeClusterBlockTest.java | 12 +- .../client/node/BulkNodeDuplicateIDTest.java | 10 +- .../client/node/BulkNodeIndexAliasTest.java | 10 +- .../client/node/BulkNodeReplicaTest.java | 26 +- .../node/BulkNodeUpdateReplicaLevelTest.java | 12 +- .../transport/BulkTransportClientTest.java | 74 +++-- .../BulkTransportDuplicateIDTest.java | 13 +- .../transport/BulkTransportReplicaTest.java | 16 +- .../BulkTransportUpdateReplicaLevelTest.java | 15 +- .../extras/client/AbstractClient.java | 28 +- .../extras/client/BulkProcessor.java | 17 +- .../extras/client/NetworkUtils.java | 6 +- .../extras/client/node/BulkNodeClient.java | 19 +- .../client/transport/BulkTransportClient.java | 32 +- .../client/transport/TransportClient.java | 294 +++++++++++------- 24 files changed, 485 insertions(+), 387 deletions(-) rename src/integration-test/java/org/xbib/elasticsearch/{NodeTestUtils.java => NodeTestBase.java} (60%) diff --git a/README.adoc b/README.adoc index 852224a..aa64451 100644 --- a/README.adoc +++ b/README.adoc @@ -99,7 +99,7 @@ You will need Java 8, although Elasticsearch 2.x requires Java 7. Java 7 is not ## Dependencies This project depends only on https://github.com/xbib/metrics which is a slim version of Coda Hale's metrics library, -and Elasticsearch. +Elasticsearch, and Log4j2 API. ## How to decode the Elasticsearch version diff --git a/build.gradle b/build.gradle index bc9dc21..33d34f8 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'org.xbib' -version = '2.2.1.1' +version = '5.0.1.0' printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", @@ -56,11 +56,10 @@ configurations { dependencies { compile "org.xbib:metrics:1.0.0" - compile "org.elasticsearch:elasticsearch:2.2.1" - testCompile "net.java.dev.jna:jna:4.1.0" + compile "org.elasticsearch.client:transport:5.0.1" + compile "org.apache.logging.log4j:log4j-api:2.7" testCompile "junit:junit:4.12" testCompile "org.apache.logging.log4j:log4j-core:2.7" - testCompile "org.apache.logging.log4j:log4j-slf4j-impl:2.7" wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' } @@ -69,7 +68,7 @@ tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:all" << "-profile" << "compact3" } -task integrationTest(type: Test) { +task integrationTest(type: Test, group: 'verification') { include '**/MiscTestSuite.class' include '**/BulkNodeTestSuite.class' include '**/BulkTransportTestSuite.class' @@ -81,7 +80,7 @@ task integrationTest(type: Test) { classpath += sourceSets.integrationTest.output outputs.upToDateWhen { false } systemProperty 'path.home', projectDir.absolutePath - testLogging.showStandardStreams = true + testLogging.showStandardStreams = false } integrationTest.mustRunAfter test diff --git a/src/integration-test/java/org/elasticsearch/node/MockNode.java b/src/integration-test/java/org/elasticsearch/node/MockNode.java index b0c02eb..10c9e86 100644 --- a/src/integration-test/java/org/elasticsearch/node/MockNode.java +++ b/src/integration-test/java/org/elasticsearch/node/MockNode.java @@ -1,6 +1,5 @@ package org.elasticsearch.node; -import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.plugins.Plugin; @@ -21,14 +20,14 @@ public class MockNode extends Node { super(settings); } - public MockNode(Settings settings, Collection> classpathPlugins) { - super(InternalSettingsPreparer.prepareEnvironment(settings, null), Version.CURRENT, classpathPlugins); - } - public MockNode(Settings settings, Class classpathPlugin) { this(settings, list(classpathPlugin)); } + public MockNode(Settings settings, Collection> classpathPlugins) { + super(InternalSettingsPreparer.prepareEnvironment(settings, null), classpathPlugins); + } + private static Collection> list(Class classpathPlugin) { Collection> list = new ArrayList<>(); list.add(classpathPlugin); diff --git a/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java b/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java index 970268e..0c683c1 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java @@ -4,16 +4,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.carrotsearch.hppc.cursors.ObjectCursor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesAction; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequestBuilder; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; -import org.elasticsearch.cluster.metadata.AliasAction; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.junit.Test; import java.io.IOException; @@ -27,9 +26,9 @@ import java.util.regex.Pattern; /** * */ -public class AliasTest extends NodeTestUtils { +public class AliasTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger(AliasTest.class.getName()); + private static final Logger logger = LogManager.getLogger(AliasTest.class.getName()); @Test public void testAlias() throws IOException { @@ -37,11 +36,9 @@ public class AliasTest extends NodeTestUtils { client("1").admin().indices().create(indexRequest).actionGet(); // put alias IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); - String[] indices = new String[]{"test"}; - String[] aliases = new String[]{"test_alias"}; - IndicesAliasesRequest.AliasActions aliasAction = - new IndicesAliasesRequest.AliasActions(AliasAction.Type.ADD, indices, aliases); - indicesAliasesRequest.addAliasAction(aliasAction); + indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add() + .index("test").alias("test_alias") + ); client("1").admin().indices().aliases(indicesAliasesRequest).actionGet(); // get alias GetAliasesRequest getAliasesRequest = new GetAliasesRequest(Strings.EMPTY_ARRAY); @@ -62,11 +59,10 @@ public class AliasTest extends NodeTestUtils { indexRequest = new CreateIndexRequest("test20160103"); client("1").admin().indices().create(indexRequest).actionGet(); IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); - String[] indices = new String[]{"test20160101", "test20160102", "test20160103"}; - String[] aliases = new String[]{alias}; - IndicesAliasesRequest.AliasActions aliasAction = - new IndicesAliasesRequest.AliasActions(AliasAction.Type.ADD, indices, aliases); - indicesAliasesRequest.addAliasAction(aliasAction); + indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add() + .indices("test20160101", "test20160102", "test20160103") + .alias(alias) + ); client("1").admin().indices().aliases(indicesAliasesRequest).actionGet(); GetAliasesRequestBuilder getAliasesRequestBuilder = new GetAliasesRequestBuilder(client("1"), diff --git a/src/integration-test/java/org/xbib/elasticsearch/NodeTestUtils.java b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java similarity index 60% rename from src/integration-test/java/org/xbib/elasticsearch/NodeTestUtils.java rename to src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java index d098332..f68a729 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/NodeTestUtils.java +++ b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java @@ -1,8 +1,7 @@ package org.xbib.elasticsearch; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; - -import org.elasticsearch.ElasticsearchTimeoutException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; @@ -10,13 +9,14 @@ import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.client.support.AbstractClient; import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.common.transport.LocalTransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.node.MockNode; import org.elasticsearch.node.Node; +import org.elasticsearch.node.NodeValidationException; +import org.elasticsearch.transport.Netty4Plugin; import org.junit.After; import org.junit.Before; import org.xbib.elasticsearch.extras.client.NetworkUtils; @@ -32,13 +32,13 @@ import java.util.concurrent.atomic.AtomicInteger; /** * */ -public class NodeTestUtils { +public class NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger("test"); + protected static final Logger logger = LogManager.getLogger("test"); - private static Random random = new Random(); + private static final Random random = new Random(); - private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz").toCharArray(); + private static final char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz").toCharArray(); private Map nodes = new HashMap<>(); @@ -46,59 +46,39 @@ public class NodeTestUtils { private AtomicInteger counter = new AtomicInteger(); - private String cluster; + private String clustername; private String host; private int port; - private static void deleteFiles() throws IOException { - Path directory = Paths.get(System.getProperty("path.home") + "/data"); - Files.walkFileTree(directory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - - }); - - } - @Before public void startNodes() { try { - logger.info("starting"); + logger.info("settings cluster name"); setClusterName(); + logger.info("starting nodes"); startNode("1"); findNodeAddress(); - try { - ClusterHealthResponse healthResponse = client("1").execute(ClusterHealthAction.INSTANCE, - new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.GREEN) - .timeout(TimeValue.timeValueSeconds(30))).actionGet(); - if (healthResponse != null && healthResponse.isTimedOut()) { - throw new IOException("cluster state is " + healthResponse.getStatus().name() - + ", from here on, everything will fail!"); - } - } catch (ElasticsearchTimeoutException e) { - throw new IOException("cluster does not respond to health request, cowardly refusing to continue"); + ClusterHealthResponse healthResponse = client("1").execute(ClusterHealthAction.INSTANCE, + new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.GREEN) + .timeout(TimeValue.timeValueSeconds(30))).actionGet(); + if (healthResponse != null && healthResponse.isTimedOut()) { + throw new IOException("cluster state is " + healthResponse.getStatus().name() + + ", from here on, everything will fail!"); } + logger.info("nodes are started"); } catch (Throwable t) { - logger.error("startNodes failed", t); + logger.error("start of nodes failed", t); } } @After public void stopNodes() { try { + logger.info("stopping nodes"); closeNodes(); - } catch (Exception e) { + } catch (Throwable e) { logger.error("can not close nodes", e); } finally { try { @@ -114,37 +94,43 @@ public class NodeTestUtils { } protected void setClusterName() { - this.cluster = "test-helper-cluster-" + this.clustername = "test-helper-cluster-" + NetworkUtils.getLocalAddress().getHostName() + "-" + System.getProperty("user.name") + "-" + counter.incrementAndGet(); } protected String getClusterName() { - return cluster; + return clustername; } - protected Settings getSettings() { - return settingsBuilder() - .put("host", host) - .put("port", port) - .put("cluster.name", cluster) + protected Settings getNodeSettings() { + String hostname = NetworkUtils.getLocalAddress().getHostName(); + return Settings.builder() + .put("cluster.name", clustername) + // required to build a cluster, replica tests will test this. + .put("discovery.zen.ping.unicast.hosts", hostname) + .put("transport.type", Netty4Plugin.NETTY_TRANSPORT_NAME) + .put("network.host", hostname) + .put("http.enabled", false) .put("path.home", getHome()) + // maximum five nodes on same host + .put("node.max_local_storage_nodes", 5) + .put("thread_pool.bulk.size", Runtime.getRuntime().availableProcessors()) + // default is 50 which is too low + .put("thread_pool.bulk.queue_size", 16 * Runtime.getRuntime().availableProcessors()) .build(); } - protected Settings getNodeSettings() { - return settingsBuilder() - .put("cluster.name", cluster) - .put("cluster.routing.schedule", "50ms") - .put("cluster.routing.allocation.disk.threshold_enabled", false) - .put("discovery.zen.multicast.enabled", true) - .put("discovery.zen.multicast.ping_timeout", "5s") - .put("http.enabled", true) - .put("threadpool.bulk.size", Runtime.getRuntime().availableProcessors()) - .put("threadpool.bulk.queue_size", 16 * Runtime.getRuntime().availableProcessors()) // default is 50, too low - .put("index.number_of_replicas", 0) - .put("path.home", getHome()) + + protected Settings getClientSettings() { + if (host == null) { + throw new IllegalStateException("host is null"); + } + // the host to which transport client should connect to + return Settings.builder() + .put("cluster.name", clustername) + .put("host", host + ":" + port) .build(); } @@ -153,7 +139,11 @@ public class NodeTestUtils { } public void startNode(String id) throws IOException { - buildNode(id).start(); + try { + buildNode(id).start(); + } catch (NodeValidationException e) { + throw new IOException(e); + } } public AbstractClient client(String id) { @@ -179,22 +169,30 @@ public class NodeTestUtils { protected void findNodeAddress() { NodesInfoRequest nodesInfoRequest = new NodesInfoRequest().transport(true); NodesInfoResponse response = client("1").admin().cluster().nodesInfo(nodesInfoRequest).actionGet(); - Object obj = response.iterator().next().getTransport().getAddress() + Object obj = response.getNodes().iterator().next().getTransport().getAddress() .publishAddress(); if (obj instanceof InetSocketTransportAddress) { InetSocketTransportAddress address = (InetSocketTransportAddress) obj; host = address.address().getHostName(); port = address.address().getPort(); + } else if (obj instanceof LocalTransportAddress) { + LocalTransportAddress address = (LocalTransportAddress) obj; + host = address.getHost(); + port = address.getPort(); + } else { + logger.info("class=" + obj.getClass()); + } + if (host == null) { + throw new IllegalArgumentException("host not found"); } } private Node buildNode(String id) throws IOException { - Settings nodeSettings = settingsBuilder() + Settings nodeSettings = Settings.builder() .put(getNodeSettings()) - .put("name", id) .build(); logger.info("settings={}", nodeSettings.getAsMap()); - Node node = new MockNode(nodeSettings); + Node node = new MockNode(nodeSettings, Netty4Plugin.class); AbstractClient client = (AbstractClient) node.client(); nodes.put(id, node); clients.put(id, client); @@ -210,4 +208,22 @@ public class NodeTestUtils { } return new String(buf); } + + private static void deleteFiles() throws IOException { + Path directory = Paths.get(System.getProperty("path.home") + "/data"); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + }); + } } diff --git a/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java b/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java index 8d1276a..efed10c 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java @@ -4,13 +4,13 @@ import static org.elasticsearch.client.Requests.indexRequest; import static org.elasticsearch.client.Requests.refreshRequest; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Client; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortOrder; @@ -19,9 +19,9 @@ import org.junit.Test; /** * */ -public class SearchTest extends NodeTestUtils { +public class SearchTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger("test"); + private static final Logger logger = LogManager.getLogger(SearchTest.class.getName()); @Test public void testSearch() throws Exception { @@ -43,7 +43,8 @@ public class SearchTest extends NodeTestUtils { .field("user8", "kimchy") .field("user9", "kimchy") .field("rowcount", i) - .field("rs", 1234))); + .field("rs", 1234) + .endObject())); } client.bulk(builder.request()).actionGet(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java b/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java index 0af13df..70f1373 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java @@ -1,28 +1,32 @@ package org.xbib.elasticsearch; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.junit.Assert.assertEquals; +import org.elasticsearch.action.admin.indices.create.CreateIndexAction; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Settings; import org.junit.Test; /** * */ -public class SimpleTest extends NodeTestUtils { +public class SimpleTest extends NodeTestBase { protected Settings getNodeSettings() { - return settingsBuilder() - .put("path.home", System.getProperty("path.home")) - .put("index.analysis.analyzer.default.filter.0", "lowercase") - .put("index.analysis.analyzer.default.filter.1", "trim") - .put("index.analysis.analyzer.default.tokenizer", "keyword") + return Settings.builder() + .put("cluster.name", getClusterName()) + .put("discovery.type", "local") + .put("transport.type", "local") + .put("http.enabled", false) + .put("path.home", getHome()) + .put("node.max_local_storage_nodes", 5) .build(); } @@ -35,6 +39,15 @@ public class SimpleTest extends NodeTestUtils { } catch (Exception e) { // ignore } + CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client("1"), CreateIndexAction.INSTANCE) + .setIndex("test") + .setSettings(Settings.builder() + .put("index.analysis.analyzer.default.filter.0", "lowercase") + .put("index.analysis.analyzer.default.filter.1", "trim") + .put("index.analysis.analyzer.default.tokenizer", "keyword") + .build()); + createIndexRequestBuilder.execute().actionGet(); + IndexRequestBuilder indexRequestBuilder = new IndexRequestBuilder(client("1"), IndexAction.INSTANCE); indexRequestBuilder .setIndex("test") @@ -42,7 +55,7 @@ public class SimpleTest extends NodeTestUtils { .setId("1") .setSource(jsonBuilder().startObject().field("field", "1%2fPJJP3JV2C24iDfEu9XpHBaYxXh%2fdHTbmchB35SDznXO2g8Vz4D7GTIvY54iMiX_149c95f02a8").endObject()) - .setRefresh(true) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .execute() .actionGet(); String doc = client("1").prepareSearch("test") diff --git a/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java b/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java index 6e252d1..d412654 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java @@ -1,10 +1,10 @@ package org.xbib.elasticsearch; import static org.elasticsearch.client.Requests.indexRequest; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.query.QueryBuilder; @@ -15,17 +15,16 @@ import java.io.IOException; /** * */ -public class WildcardTest extends NodeTestUtils { +public class WildcardTest extends NodeTestBase { protected Settings getNodeSettings() { - return settingsBuilder() + return Settings.builder() .put("cluster.name", getClusterName()) - .put("cluster.routing.allocation.disk.threshold_enabled", false) - .put("discovery.zen.multicast.enabled", false) + .put("discovery.type", "local") + .put("transport.type", "local") .put("http.enabled", false) - .put("path.home", System.getProperty("path.home")) - .put("index.number_of_shards", 1) - .put("index.number_of_replicas", 0) + .put("path.home", getHome()) + .put("node.max_local_storage_nodes", 5) .build(); } @@ -51,7 +50,8 @@ public class WildcardTest extends NodeTestUtils { client.index(indexRequest() .index("index").type("type").id(id) .source(jsonBuilder().startObject().field("field", fieldValue).endObject()) - .refresh(true)).actionGet(); + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)) + .actionGet(); } private long count(Client client, QueryBuilder queryBuilder) { diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java index e01ca67..a1165ff 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java @@ -4,36 +4,39 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.util.ExecutorServices; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.junit.Before; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * */ -public class BulkNodeClientTest extends NodeTestUtils { +public class BulkNodeClientTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkNodeClientTest.class.getSimpleName()); + private static final Logger logger = LogManager.getLogger(BulkNodeClientTest.class.getName()); private static final Long MAX_ACTIONS = 1000L; @@ -165,18 +168,15 @@ public class BulkNodeClientTest extends NodeTestUtils { .toBulkNodeClient(client("1")); try { client.newIndex("test") - .startBulk("test", -1, 1000); - ThreadPoolExecutor pool = EsExecutors.newFixed("bulk-nodeclient-test", maxthreads, 30, - EsExecutors.daemonThreadFactory("bulk-nodeclient-test")); + .startBulk("test", 30 * 1000, 1000); + ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); final CountDownLatch latch = new CountDownLatch(maxthreads); for (int i = 0; i < maxthreads; i++) { - pool.execute(new Runnable() { - public void run() { - for (int i = 0; i < maxloop; i++) { - client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); - } - latch.countDown(); + executorService.execute(() -> { + for (int i1 = 0; i1 < maxloop; i1++) { + client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); } + latch.countDown(); }); } logger.info("waiting for max 30 seconds..."); @@ -184,8 +184,8 @@ public class BulkNodeClientTest extends NodeTestUtils { logger.info("flush..."); client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); - logger.info("got all responses, thread pool shutdown..."); - pool.shutdown(); + logger.info("got all responses, executor service shutdown..."); + executorService.shutdown(); logger.info("pool is shut down"); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java index 09c628d..2b79ddc 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java @@ -1,24 +1,24 @@ package org.xbib.elasticsearch.extras.client.node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.cluster.block.ClusterBlockException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.junit.Before; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; /** * */ -public class BulkNodeClusterBlockTest extends NodeTestUtils { +public class BulkNodeClusterBlockTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger("test"); + private static final Logger logger = LogManager.getLogger(BulkNodeClusterBlockTest.class.getName()); @Before public void startNodes() { @@ -34,7 +34,7 @@ public class BulkNodeClusterBlockTest extends NodeTestUtils { } protected Settings getNodeSettings() { - return Settings.settingsBuilder() + return Settings.builder() .put(super.getNodeSettings()) .put("discovery.zen.minimum_master_nodes", 2) // block until we have two nodes .build(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java index 7d8ba1f..d7e9a30 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java @@ -1,13 +1,13 @@ package org.xbib.elasticsearch.extras.client.node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.unit.TimeValue; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -18,9 +18,9 @@ import static org.junit.Assert.*; /** * */ -public class BulkNodeDuplicateIDTest extends NodeTestUtils { +public class BulkNodeDuplicateIDTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkNodeDuplicateIDTest.class.getSimpleName()); + private static final Logger logger = LogManager.getLogger(BulkNodeDuplicateIDTest.class.getName()); private static final Long MAX_ACTIONS = 1000L; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java index d4b19b0..a6f00a4 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java @@ -1,13 +1,13 @@ package org.xbib.elasticsearch.extras.client.node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilders; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.IndexAliasAdder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; @@ -22,9 +22,9 @@ import static org.junit.Assert.assertFalse; /** * */ -public class BulkNodeIndexAliasTest extends NodeTestUtils { +public class BulkNodeIndexAliasTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkNodeIndexAliasTest.class.getSimpleName()); + private static final Logger logger = LogManager.getLogger(BulkNodeIndexAliasTest.class.getName()); @Test public void testIndexAlias() throws Exception { diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java index 93141e1..637a28e 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java @@ -1,16 +1,21 @@ package org.xbib.elasticsearch.extras.client.node; -import org.elasticsearch.action.admin.indices.stats.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.indices.stats.CommonStats; +import org.elasticsearch.action.admin.indices.stats.IndexShardStats; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequestBuilder; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.index.indexing.IndexingStats; +import org.elasticsearch.index.shard.IndexingStats; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -21,9 +26,12 @@ import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -public class BulkNodeReplicaTest extends NodeTestUtils { +/** + * + */ +public class BulkNodeReplicaTest extends NodeTestBase { - private final static ESLogger logger = ESLoggerFactory.getLogger(BulkNodeReplicaTest.class.getSimpleName()); + private final static Logger logger = LogManager.getLogger(BulkNodeReplicaTest.class.getName()); @Test public void testReplicaLevel() throws Exception { @@ -33,12 +41,12 @@ public class BulkNodeReplicaTest extends NodeTestUtils { startNode("3"); startNode("4"); - Settings settingsTest1 = Settings.settingsBuilder() + Settings settingsTest1 = Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 3) .build(); - Settings settingsTest2 = Settings.settingsBuilder() + Settings settingsTest2 = Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 1) .build(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java index b1c88fe..7f43457 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java @@ -1,12 +1,12 @@ package org.xbib.elasticsearch.extras.client.node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -17,9 +17,9 @@ import static org.junit.Assert.assertFalse; /** * */ -public class BulkNodeUpdateReplicaLevelTest extends NodeTestUtils { +public class BulkNodeUpdateReplicaLevelTest extends NodeTestBase { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkNodeUpdateReplicaLevelTest.class.getSimpleName()); + private static final Logger logger = LogManager.getLogger(BulkNodeUpdateReplicaLevelTest.class.getName()); @Test public void testUpdateReplicaLevel() throws Exception { @@ -33,7 +33,7 @@ public class BulkNodeUpdateReplicaLevelTest extends NodeTestUtils { int shardsAfterReplica; - Settings settings = Settings.settingsBuilder() + Settings settings = Settings.builder() .put("index.number_of_shards", numberOfShards) .put("index.number_of_replicas", 0) .build(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java index 0a35742..4b6dc41 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java @@ -3,15 +3,12 @@ package org.xbib.elasticsearch.extras.client.transport; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.index.query.QueryBuilders; import org.junit.Before; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -19,7 +16,8 @@ import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; @@ -28,9 +26,7 @@ import static org.junit.Assert.assertFalse; /** * */ -public class BulkTransportClientTest extends NodeTestUtils { - - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkTransportClientTest.class.getSimpleName()); +public class BulkTransportClientTest extends NodeTestBase { private static final Long MAX_ACTIONS = 1000L; @@ -47,22 +43,26 @@ public class BulkTransportClientTest extends NodeTestUtils { } @Test - public void testBulkClient() throws IOException { + public void testBulkClientIndexCreation() throws IOException { + logger.info("firing up BulkTransportClient"); final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) .toBulkTransportClient(); + logger.info("creating index"); client.newIndex("test"); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); } assertFalse(client.hasThrowable()); try { + logger.info("deleting/creating index sequence start"); client.deleteIndex("test") .newIndex("test") .deleteIndex("test"); + logger.info("deleting/creating index sequence end"); } catch (NoNodeAvailableException e) { logger.error("no node available"); } finally { @@ -76,18 +76,24 @@ public class BulkTransportClientTest extends NodeTestUtils { @Test public void testSingleDocBulkClient() throws IOException { + logger.info("firing up BulkTransportClient"); final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) .toBulkTransportClient(); try { + logger.info("creating index"); client.newIndex("test"); + logger.info("indexing one doc"); client.index("test", "test", "1", "{ \"name\" : \"Hello World\"}"); // single doc ingest + logger.info("flush"); client.flushIngest(); + logger.info("wait for responses"); client.waitForResponses(TimeValue.timeValueSeconds(30)); + logger.info("waited for responses"); } catch (InterruptedException e) { // ignore } catch (ExecutionException e) { @@ -105,10 +111,10 @@ public class BulkTransportClientTest extends NodeTestUtils { } @Test - public void testRandomDocsBulkClient() throws IOException { + public void testRandomDocsBulkClient() { long numactions = NUM_ACTIONS; final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) @@ -127,7 +133,10 @@ public class BulkTransportClientTest extends NodeTestUtils { logger.error(e.getMessage(), e); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); + } catch (Throwable t) { + logger.error("unexcepted: " + t.getMessage(), t); } finally { + logger.info("assuring {} == {}", numactions, client.getMetric().getSucceeded().getCount()); assertEquals(numactions, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); @@ -138,34 +147,36 @@ public class BulkTransportClientTest extends NodeTestUtils { } @Test - public void testThreadedRandomDocsBulkClient() throws Exception { + public void testThreadedRandomDocsBulkClient() { int maxthreads = Runtime.getRuntime().availableProcessors(); long maxactions = MAX_ACTIONS; final long maxloop = NUM_ACTIONS; - - Settings settingsForIndex = Settings.settingsBuilder() - .put("index.number_of_shards", 2) - .put("index.number_of_replicas", 1) - .build(); - + logger.info("firing up client"); final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, maxactions) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) // = disable autoflush for this test .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) .toBulkTransportClient(); try { + logger.info("new index"); + Settings settingsForIndex = Settings.builder() + .put("index.number_of_shards", 2) + .put("index.number_of_replicas", 1) + .build(); client.newIndex("test", settingsForIndex, null) .startBulk("test", -1, 1000); - ThreadPoolExecutor pool = - EsExecutors.newFixed("bulkclient-test", maxthreads, 30, EsExecutors.daemonThreadFactory("bulkclient-test")); + logger.info("pool"); + ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); final CountDownLatch latch = new CountDownLatch(maxthreads); for (int i = 0; i < maxthreads; i++) { - pool.execute(() -> { + executorService.execute(() -> { + logger.info("executing runnable"); for (int i1 = 0; i1 < maxloop; i1++) { client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); } + logger.info("done runnable"); latch.countDown(); }); } @@ -174,13 +185,16 @@ public class BulkTransportClientTest extends NodeTestUtils { logger.info("client flush ..."); client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); - logger.info("thread pool to be shut down ..."); - pool.shutdown(); - logger.info("poot shut down"); + logger.info("executor service to be shut down ..."); + executorService.shutdown(); + logger.info("executor service is shut down"); + client.stopBulk("test"); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); + } catch (Throwable t) { + logger.error("unexpected error: " + t.getMessage(), t); } finally { - client.stopBulk("test"); + logger.info("assuring {} == {}", maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); assertEquals(maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); @@ -188,8 +202,7 @@ public class BulkTransportClientTest extends NodeTestUtils { assertFalse(client.hasThrowable()); client.refreshIndex("test"); SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) - // to avoid NPE at org.elasticsearch.action.search.SearchRequest.writeTo(SearchRequest.java:580) - .setIndices("_all") + .setIndices("test") .setQuery(QueryBuilders.matchAllQuery()) .setSize(0); assertEquals(maxthreads * maxloop, @@ -197,5 +210,4 @@ public class BulkTransportClientTest extends NodeTestUtils { client.shutdown(); } } - } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java index 00a4066..574928c 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java @@ -3,11 +3,9 @@ package org.xbib.elasticsearch.extras.client.transport; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.unit.TimeValue; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -15,9 +13,10 @@ import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.junit.Assert.*; -public class BulkTransportDuplicateIDTest extends NodeTestUtils { - - private final static ESLogger logger = ESLoggerFactory.getLogger(BulkTransportDuplicateIDTest.class.getSimpleName()); +/** + * + */ +public class BulkTransportDuplicateIDTest extends NodeTestBase { private final static Long MAX_ACTIONS = 1000L; @@ -27,7 +26,7 @@ public class BulkTransportDuplicateIDTest extends NodeTestUtils { public void testDuplicateDocIDs() throws Exception { long numactions = NUM_ACTIONS; final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java index 119688e..386f212 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java @@ -4,13 +4,11 @@ import org.elasticsearch.action.admin.indices.stats.*; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.index.indexing.IndexingStats; +import org.elasticsearch.index.shard.IndexingStats; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -24,9 +22,7 @@ import static org.junit.Assert.assertFalse; /** * */ -public class BulkTransportReplicaTest extends NodeTestUtils { - - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkTransportReplicaTest.class.getSimpleName()); +public class BulkTransportReplicaTest extends NodeTestBase { @Test public void testReplicaLevel() throws Exception { @@ -36,18 +32,18 @@ public class BulkTransportReplicaTest extends NodeTestUtils { startNode("3"); startNode("4"); - Settings settingsTest1 = Settings.settingsBuilder() + Settings settingsTest1 = Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 3) .build(); - Settings settingsTest2 = Settings.settingsBuilder() + Settings settingsTest2 = Settings.builder() .put("index.number_of_shards", 2) .put("index.number_of_replicas", 1) .build(); final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) .toBulkTransportClient(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java index 8ed2c4a..331ac11 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java @@ -1,12 +1,12 @@ package org.xbib.elasticsearch.extras.client.transport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.junit.Test; -import org.xbib.elasticsearch.NodeTestUtils; +import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; @@ -17,10 +17,9 @@ import static org.junit.Assert.assertFalse; /** * */ -public class BulkTransportUpdateReplicaLevelTest extends NodeTestUtils { +public class BulkTransportUpdateReplicaLevelTest extends NodeTestBase { - private static final ESLogger logger = - ESLoggerFactory.getLogger(BulkTransportUpdateReplicaLevelTest.class.getSimpleName()); + private static final Logger logger = LogManager.getLogger(BulkTransportUpdateReplicaLevelTest.class.getName()); @Test public void testUpdateReplicaLevel() throws Exception { @@ -34,13 +33,13 @@ public class BulkTransportUpdateReplicaLevelTest extends NodeTestUtils { int shardsAfterReplica; - Settings settings = Settings.settingsBuilder() + Settings settings = Settings.builder() .put("index.number_of_shards", numberOfShards) .put("index.number_of_replicas", 0) .build(); final BulkTransportClient client = ClientBuilder.builder() - .put(getSettings()) + .put(getClientSettings()) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) .toBulkTransportClient(); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java index 877067b..f4ce3aa 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java @@ -1,5 +1,7 @@ package org.xbib.elasticsearch.extras.client; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import org.elasticsearch.ElasticsearchTimeoutException; @@ -39,8 +41,6 @@ import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.common.io.Streams; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.search.SearchHit; @@ -68,7 +68,7 @@ import java.util.regex.Pattern; */ public abstract class AbstractClient { - private static final ESLogger logger = ESLoggerFactory.getLogger(AbstractClient.class.getName()); + private static final Logger logger = LogManager.getLogger(AbstractClient.class.getName()); private Settings.Builder settingsBuilder; @@ -87,7 +87,7 @@ public abstract class AbstractClient { } public void resetSettings() { - this.settingsBuilder = Settings.settingsBuilder(); + this.settingsBuilder = Settings.builder(); settings = null; mappings = new HashMap<>(); } @@ -98,31 +98,31 @@ public abstract class AbstractClient { public void setting(String key, String value) { if (settingsBuilder == null) { - settingsBuilder = Settings.settingsBuilder(); + settingsBuilder = Settings.builder(); } settingsBuilder.put(key, value); } public void setting(String key, Boolean value) { if (settingsBuilder == null) { - settingsBuilder = Settings.settingsBuilder(); + settingsBuilder = Settings.builder(); } settingsBuilder.put(key, value); } public void setting(String key, Integer value) { if (settingsBuilder == null) { - settingsBuilder = Settings.settingsBuilder(); + settingsBuilder = Settings.builder(); } settingsBuilder.put(key, value); } public void setting(InputStream in) throws IOException { - settingsBuilder = Settings.settingsBuilder().loadFromStream(".json", in); + settingsBuilder = Settings.builder().loadFromStream(".json", in); } public Settings.Builder settingsBuilder() { - return settingsBuilder != null ? settingsBuilder : Settings.settingsBuilder(); + return settingsBuilder != null ? settingsBuilder : Settings.builder(); } public Settings settings() { @@ -130,7 +130,7 @@ public abstract class AbstractClient { return settings; } if (settingsBuilder == null) { - settingsBuilder = Settings.settingsBuilder(); + settingsBuilder = Settings.builder(); } return settingsBuilder.build(); } @@ -166,7 +166,7 @@ public abstract class AbstractClient { if (value == null) { throw new IOException("no value given"); } - Settings.Builder updateSettingsBuilder = Settings.settingsBuilder(); + Settings.Builder updateSettingsBuilder = Settings.builder(); updateSettingsBuilder.put(key, value.toString()); UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(index) .settings(updateSettingsBuilder); @@ -218,7 +218,7 @@ public abstract class AbstractClient { new ClusterStateRequestBuilder(client(), ClusterStateAction.INSTANCE).all(); ClusterStateResponse clusterStateResponse = clusterStateRequestBuilder.execute().actionGet(); String name = clusterStateResponse.getClusterName().value(); - int nodeCount = clusterStateResponse.getState().getNodes().size(); + int nodeCount = clusterStateResponse.getState().getNodes().getSize(); return name + " (" + nodeCount + " nodes connected)"; } catch (ElasticsearchTimeoutException e) { logger.warn(e.getMessage(), e); @@ -476,9 +476,9 @@ public abstract class AbstractClient { return null; } SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client(), SearchAction.INSTANCE); - SortBuilder sort = SortBuilders.fieldSort(timestampfieldname).order(SortOrder.DESC); + SortBuilder sort = SortBuilders.fieldSort(timestampfieldname).order(SortOrder.DESC); SearchResponse searchResponse = searchRequestBuilder.setIndices(index) - .addField(timestampfieldname) + .addStoredField(timestampfieldname) .setSize(1) .addSort(sort) .execute().actionGet(); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java index 814a7c7..b15c243 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java @@ -9,7 +9,6 @@ import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; @@ -51,7 +50,7 @@ public class BulkProcessor implements Closeable { private BulkProcessor(Client client, Listener listener, @Nullable String name, int concurrentRequests, int bulkActions, ByteSizeValue bulkSize, @Nullable TimeValue flushInterval) { this.bulkActions = bulkActions; - this.bulkSize = bulkSize.bytes(); + this.bulkSize = bulkSize.getBytes(); this.bulkRequest = new BulkRequest(); this.bulkRequestHandler = concurrentRequests == 0 ? @@ -176,18 +175,6 @@ public class BulkProcessor implements Closeable { executeIfNeeded(); } - public BulkProcessor add(BytesReference data, @Nullable String defaultIndex, @Nullable String defaultType) - throws Exception { - return add(data, defaultIndex, defaultType, null); - } - - public synchronized BulkProcessor add(BytesReference data, @Nullable String defaultIndex, - @Nullable String defaultType, @Nullable Object payload) throws Exception { - bulkRequest.add(data, defaultIndex, defaultType, null, null, payload, true); - executeIfNeeded(); - return this; - } - private void executeIfNeeded() { ensureOpen(); if (!isOverTheLimit()) { @@ -441,7 +428,7 @@ public class BulkProcessor implements Closeable { } @Override - public void onFailure(Throwable e) { + public void onFailure(Exception e) { try { listener.afterBulk(executionId, bulkRequest, e); } finally { diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java b/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java index 9c5ffc2..277e13a 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java @@ -1,7 +1,7 @@ package org.xbib.elasticsearch.extras.client; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.IOException; import java.net.Inet4Address; @@ -20,7 +20,7 @@ import java.util.Locale; */ public class NetworkUtils { - private static final ESLogger logger = ESLoggerFactory.getLogger(NetworkUtils.class.getName()); + private static final Logger logger = LogManager.getLogger(NetworkUtils.class.getName()); private static final String IPV4_SETTING = "java.net.preferIPv4Stack"; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java index 74a8dc4..17a0779 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java @@ -1,8 +1,8 @@ package org.xbib.elasticsearch.extras.client.node; -import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; @@ -18,13 +18,12 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.Client; import org.elasticsearch.client.ElasticsearchClient; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.env.Environment; import org.elasticsearch.node.Node; +import org.elasticsearch.node.NodeValidationException; import org.elasticsearch.plugins.Plugin; import org.xbib.elasticsearch.extras.client.AbstractClient; import org.xbib.elasticsearch.extras.client.BulkControl; @@ -44,7 +43,7 @@ import java.util.concurrent.TimeUnit; */ public class BulkNodeClient extends AbstractClient implements ClientMethods { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkNodeClient.class.getName()); + private static final Logger logger = LogManager.getLogger(BulkNodeClient.class.getName()); private int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; @@ -216,7 +215,11 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { version, effectiveSettings.getAsMap()); Collection> plugins = Collections.emptyList(); this.node = new BulkNode(new Environment(effectiveSettings), plugins); - node.start(); + try { + node.start(); + } catch (NodeValidationException e) { + throw new IOException(e); + } this.client = node.client(); } } @@ -389,7 +392,7 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { } if (control != null && control.indices() != null && !control.indices().isEmpty()) { logger.debug("stopping bulk mode for indices {}...", control.indices()); - for (String index : ImmutableSet.copyOf(control.indices())) { + for (String index : control.indices()) { stopBulk(index); } metric.stop(); @@ -505,7 +508,7 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { private class BulkNode extends Node { BulkNode(Environment env, Collection> classpathPlugins) { - super(env, Version.CURRENT, classpathPlugins); + super(env, classpathPlugins); } } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java index b03aeef..be51e62 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java @@ -1,6 +1,7 @@ package org.xbib.elasticsearch.extras.client.transport; -import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequestBuilder; @@ -21,12 +22,13 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; +import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.env.Environment; +import org.elasticsearch.transport.Netty4Plugin; import org.xbib.elasticsearch.extras.client.AbstractClient; import org.xbib.elasticsearch.extras.client.BulkControl; import org.xbib.elasticsearch.extras.client.BulkMetric; @@ -39,6 +41,7 @@ import java.io.InputStream; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -49,7 +52,9 @@ import java.util.concurrent.TimeUnit; */ public class BulkTransportClient extends AbstractClient implements ClientMethods { - private static final ESLogger logger = ESLoggerFactory.getLogger(BulkTransportClient.class.getName()); + private static final Logger logger = LogManager.getLogger(BulkTransportClient.class.getName()); + + private static final Settings DEFAULT_SETTINGS = Settings.builder().put("transport.type.default", "local").build(); private int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; @@ -165,6 +170,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods builder.setBulkSize(maxVolumePerRequest); } this.bulkProcessor = builder.build(); + // aut-connect here try { Collection addrs = findAddresses(settings); if (!connect(addrs, settings.getAsBoolean("autodiscover", false))) { @@ -205,9 +211,10 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods + " " + System.getProperty("java.vm.version"); logger.info("creating transport client on {} with effective settings {}", version, settings.getAsMap()); - this.client = TransportClient.builder() - .settings(settings) - .build(); + this.client = new TransportClient(Settings.builder() + .put("cluster.name", settings.get("cluster.name")) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .build(), Collections.singletonList(Netty4Plugin.class)); this.ignoreBulkErrors = settings.getAsBoolean("ignoreBulkErrors", true); } } @@ -313,7 +320,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods if (control == null) { return this; } - if (!control.isBulk(index)) { + if (!control.isBulk(index) && startRefreshIntervalSeconds > 0L && stopRefreshIntervalSeconds > 0L) { control.startBulk(index, startRefreshIntervalSeconds, stopRefreshIntervalSeconds); updateIndexSetting(index, "refresh_interval", startRefreshIntervalSeconds + "s"); } @@ -326,7 +333,10 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods return this; } if (control.isBulk(index)) { - updateIndexSetting(index, "refresh_interval", control.getStopBulkRefreshIntervals().get(index) + "s"); + long secs = control.getStopBulkRefreshIntervals().get(index); + if (secs > 0L) { + updateIndexSetting(index, "refresh_interval", secs + "s"); + } control.finishBulk(index); } return this; @@ -461,7 +471,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods } if (control != null && control.indices() != null && !control.indices().isEmpty()) { logger.debug("stopping bulk mode for indices {}...", control.indices()); - for (String index : ImmutableSet.copyOf(control.indices())) { + for (String index : control.indices()) { stopBulk(index); } metric.stop(); @@ -485,7 +495,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods } private Settings findSettings() { - Settings.Builder settingsBuilder = Settings.settingsBuilder(); + Settings.Builder settingsBuilder = Settings.builder(); settingsBuilder.put("host", "localhost"); try { String hostname = NetworkUtils.getLocalAddress().getHostName(); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java index 3912ce7..2ded1ab 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java @@ -1,9 +1,10 @@ package org.xbib.elasticsearch.extras.client.transport; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; -import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionListener; @@ -16,39 +17,41 @@ import org.elasticsearch.action.TransportActionNodeProxy; import org.elasticsearch.action.admin.cluster.node.liveness.LivenessRequest; import org.elasticsearch.action.admin.cluster.node.liveness.LivenessResponse; import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction; -import org.elasticsearch.cache.recycler.PageCacheRecycler; import org.elasticsearch.client.support.AbstractClient; -import org.elasticsearch.client.support.Headers; -import org.elasticsearch.client.transport.ClientTransportModule; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.ClusterNameModule; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.LifecycleComponent; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Injector; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.ModulesBuilder; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.indices.breaker.CircuitBreakerModule; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.node.Node; import org.elasticsearch.node.internal.InternalSettingsPreparer; +import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.PluginsModule; import org.elasticsearch.plugins.PluginsService; +import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.search.SearchModule; +import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.threadpool.ThreadPoolModule; import org.elasticsearch.transport.FutureTransportResponseHandler; -import org.elasticsearch.transport.TransportModule; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; +import java.io.Closeable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -59,6 +62,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * Stripped-down transport client without node sampling and without retrying. @@ -73,17 +77,13 @@ public class TransportClient extends AbstractClient { private final Injector injector; - private final ProxyActionMap proxyActionMap; - private final long pingTimeout; private final ClusterName clusterName; private final TransportService transportService; - private final Version minCompatibilityVersion; - - private final Headers headers; + private final ProxyActionMap proxy; private final AtomicInteger tempNodeId = new AtomicInteger(); @@ -99,20 +99,41 @@ public class TransportClient extends AbstractClient { private volatile boolean closed; - private TransportClient(Injector injector) { - super(injector.getInstance(Settings.class), injector.getInstance(ThreadPool.class), - injector.getInstance(Headers.class)); - this.injector = injector; - this.clusterName = injector.getInstance(ClusterName.class); - this.transportService = injector.getInstance(TransportService.class); - this.minCompatibilityVersion = injector.getInstance(Version.class).minimumCompatibilityVersion(); - this.headers = injector.getInstance(Headers.class); - this.pingTimeout = this.settings.getAsTime("client.transport.ping_timeout", timeValueSeconds(5)).millis(); - this.proxyActionMap = injector.getInstance(ProxyActionMap.class); + + /** + * Creates a new TransportClient with the given settings and plugins. + * @param settings settings + */ + public TransportClient(Settings settings) { + this(buildTemplate(settings, Settings.EMPTY, Collections.emptyList())); + } + + /** + * Creates a new TransportClient with the given settings and plugins. + * @param settings settings + * @param plugins plugins + */ + public TransportClient(Settings settings, Collection> plugins) { + this(buildTemplate(settings, Settings.EMPTY, plugins)); + } + + /** + * Creates a new TransportClient with the given settings, defaults and plugins. + * @param settings the client settings + * @param defaultSettings default settings that are merged after the plugins have added it's additional settings. + * @param plugins the client plugins + */ + protected TransportClient(Settings settings, Settings defaultSettings, Collection> plugins) { + this(buildTemplate(settings, defaultSettings, plugins)); } - public static Builder builder() { - return new Builder(); + private TransportClient(ClientTemplate template) { + super(template.getSettings(), template.getThreadPool()); + this.injector = template.injector; + this.clusterName = new ClusterName(template.getSettings().get("cluster.name", "elasticsearch")); + this.transportService = injector.getInstance(TransportService.class); + this.pingTimeout = this.settings.getAsTime("client.transport.ping_timeout", timeValueSeconds(5)).millis(); + this.proxy = template.proxy; } /** @@ -123,7 +144,7 @@ public class TransportClient extends AbstractClient { public List transportAddresses() { List lstBuilder = new ArrayList<>(); for (DiscoveryNode listedNode : listedNodes) { - lstBuilder.add(listedNode.address()); + lstBuilder.add(listedNode.getAddress()); } return Collections.unmodifiableList(lstBuilder); } @@ -170,7 +191,7 @@ public class TransportClient extends AbstractClient { public TransportClient addDiscoveryNodes(DiscoveryNodes discoveryNodes) { Collection addresses = new ArrayList<>(); for (DiscoveryNode discoveryNode : discoveryNodes) { - addresses.add((InetSocketTransportAddress) discoveryNode.address()); + addresses.add((InetSocketTransportAddress) discoveryNode.getAddress()); } addTransportAddresses(addresses); return this; @@ -185,7 +206,7 @@ public class TransportClient extends AbstractClient { for (TransportAddress transportAddress : transportAddresses) { boolean found = false; for (DiscoveryNode otherNode : listedNodes) { - if (otherNode.address().equals(transportAddress)) { + if (otherNode.getAddress().equals(transportAddress)) { found = true; logger.debug("address [{}] already exists with [{}], ignoring...", transportAddress, otherNode); break; @@ -202,7 +223,7 @@ public class TransportClient extends AbstractClient { discoveryNodeList.addAll(listedNodes()); for (TransportAddress transportAddress : filtered) { DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeId.incrementAndGet(), transportAddress, - minCompatibilityVersion); + Version.CURRENT.minimumCompatibilityVersion()); logger.debug("adding address [{}]", node); discoveryNodeList.add(node); } @@ -225,7 +246,7 @@ public class TransportClient extends AbstractClient { } List builder = new ArrayList<>(); for (DiscoveryNode otherNode : listedNodes) { - if (!otherNode.address().equals(transportAddress)) { + if (!otherNode.getAddress().equals(transportAddress)) { builder.add(otherNode); } else { logger.debug("removing address [{}]", otherNode); @@ -253,7 +274,7 @@ public class TransportClient extends AbstractClient { nodes = Collections.emptyList(); } injector.getInstance(TransportService.class).close(); - for (Class plugin : injector.getInstance(PluginsService.class).nodeServices()) { + for (Class plugin : injector.getInstance(PluginsService.class).getGuiceServiceClasses()) { injector.getInstance(plugin).close(); } try { @@ -261,7 +282,6 @@ public class TransportClient extends AbstractClient { } catch (Exception e) { logger.debug(e.getMessage(), e); } - injector.getInstance(PageCacheRecycler.class).close(); } private void connect() { @@ -279,7 +299,7 @@ public class TransportClient extends AbstractClient { } try { LivenessResponse livenessResponse = transportService.submitRequest(listedNode, - TransportLivenessAction.NAME, headers.applyTo(new LivenessRequest()), + TransportLivenessAction.NAME, new LivenessRequest(), TransportRequestOptions.builder().withType(TransportRequestOptions.Type.STATE) .withTimeout(pingTimeout).build(), new FutureTransportResponseHandler() { @@ -293,9 +313,10 @@ public class TransportClient extends AbstractClient { newFilteredNodes.add(listedNode); } else if (livenessResponse.getDiscoveryNode() != null) { DiscoveryNode nodeWithInfo = livenessResponse.getDiscoveryNode(); - newNodes.add(new DiscoveryNode(nodeWithInfo.name(), nodeWithInfo.id(), nodeWithInfo.getHostName(), - nodeWithInfo.getHostAddress(), listedNode.address(), nodeWithInfo.attributes(), - nodeWithInfo.version())); + newNodes.add(new DiscoveryNode(nodeWithInfo.getName(), nodeWithInfo.getId(), + nodeWithInfo.getEphemeralId(), nodeWithInfo.getHostName(), + nodeWithInfo.getHostAddress(), listedNode.getAddress(), nodeWithInfo.getAttributes(), + nodeWithInfo.getRoles(), nodeWithInfo.getVersion())); } else { logger.debug("node {} didn't return any discovery info, temporarily using transport discovery node", listedNode); @@ -324,9 +345,9 @@ public class TransportClient extends AbstractClient { @Override @SuppressWarnings({"unchecked", "rawtypes"}) - protected > + protected , S extends ActionResponse, T extends ActionRequestBuilder> void doExecute(Action action, final R request, final ActionListener listener) { - final TransportActionNodeProxy proxyAction = proxyActionMap.getProxies().get(action); + final TransportActionNodeProxy proxyAction = proxy.getProxies().get(action); if (proxyAction == null) { throw new IllegalStateException("undefined action " + action); } @@ -348,98 +369,137 @@ public class TransportClient extends AbstractClient { } /** - * + * The {@link ProxyActionMap} must be declared public. */ - public static class Builder { + @SuppressWarnings({"unchecked", "rawtypes"}) + private static class ProxyActionMap { - private Settings settings = Settings.EMPTY; - private List> pluginClasses = new ArrayList<>(); + private final Map proxies; - public Builder settings(Settings.Builder settings) { - return settings(settings.build()); + public ProxyActionMap(Settings settings, TransportService transportService, List actions) { + MapBuilder actionsBuilder = new MapBuilder<>(); + for (GenericAction action : actions) { + if (action instanceof Action) { + actionsBuilder.put((Action) action, new TransportActionNodeProxy(settings, action, transportService)); + } + } + this.proxies = actionsBuilder.immutableMap(); } - public Builder settings(Settings settings) { - this.settings = settings; - return this; + Map getProxies() { + return proxies; } + } - public Builder addPlugin(Class pluginClass) { - pluginClasses.add(pluginClass); - return this; - } - public TransportClient build() { - Settings transportClientSettings = settingsBuilder() - .put("transport.ping.schedule", this.settings.get("ping.interval", "30s")) - .put(InternalSettingsPreparer.prepareSettings(this.settings)) - .put("network.server", false) - .put("node.client", true) - .put(CLIENT_TYPE_SETTING, CLIENT_TYPE) - .build(); - PluginsService pluginsService = new PluginsService(transportClientSettings, null, null, pluginClasses); - this.settings = pluginsService.updatedSettings(); - Version version = Version.CURRENT; - final ThreadPool threadPool = new ThreadPool(transportClientSettings); - boolean success = false; - try { - ModulesBuilder modules = new ModulesBuilder(); - modules.add(new Version.Module(version)); - // plugin modules must be added here, before others or we can get crazy injection errors... - for (Module pluginModule : pluginsService.nodeModules()) { - modules.add(pluginModule); - } - modules.add(new PluginsModule(pluginsService)); - modules.add(new SettingsModule(this.settings)); - modules.add(new NetworkModule()); - modules.add(new ClusterNameModule(this.settings)); - modules.add(new ThreadPoolModule(threadPool)); - modules.add(new TransportModule(this.settings)); - modules.add(new SearchModule() { - @Override - protected void configure() { - // noop - } - }); - modules.add(new ActionModule(true)); - modules.add(new ClientTransportModule()); - modules.add(new CircuitBreakerModule(this.settings)); - pluginsService.processModules(modules); - Injector injector = modules.createInjector(); - injector.getInstance(TransportService.class).start(); - TransportClient transportClient = new TransportClient(injector); - success = true; - return transportClient; - } finally { - if (!success) { - ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); - } + private static ClientTemplate buildTemplate(Settings providedSettings, Settings defaultSettings, + Collection> plugins) { + if (!Node.NODE_NAME_SETTING.exists(providedSettings)) { + providedSettings = Settings.builder().put(providedSettings).put(Node.NODE_NAME_SETTING.getKey(), "_client_").build(); + } + final PluginsService pluginsService = newPluginService(providedSettings, plugins); + final Settings settings = Settings.builder().put(defaultSettings).put(pluginsService.updatedSettings()).build(); + final List resourcesToClose = new ArrayList<>(); + final ThreadPool threadPool = new ThreadPool(settings); + resourcesToClose.add(() -> ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS)); + final NetworkService networkService = new NetworkService(settings, Collections.emptyList()); + try { + final List> additionalSettings = new ArrayList<>(); + final List additionalSettingsFilter = new ArrayList<>(); + additionalSettings.addAll(pluginsService.getPluginSettings()); + additionalSettingsFilter.addAll(pluginsService.getPluginSettingsFilter()); + for (final ExecutorBuilder builder : threadPool.builders()) { + additionalSettings.addAll(builder.getRegisteredSettings()); + } + SettingsModule settingsModule = new SettingsModule(settings, additionalSettings, additionalSettingsFilter); + NetworkModule networkModule = new NetworkModule(networkService, settings, true); + SearchModule searchModule = new SearchModule(settings, true, pluginsService.filterPlugins(SearchPlugin.class)); + List entries = new ArrayList<>(); + entries.addAll(networkModule.getNamedWriteables()); + entries.addAll(searchModule.getNamedWriteables()); + entries.addAll(pluginsService.filterPlugins(Plugin.class).stream() + .flatMap(p -> p.getNamedWriteables().stream()) + .collect(Collectors.toList())); + NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(entries); + + ModulesBuilder modules = new ModulesBuilder(); + // plugin modules must be added here, before others or we can get crazy injection errors... + for (Module pluginModule : pluginsService.createGuiceModules()) { + modules.add(pluginModule); } + modules.add(networkModule); + modules.add(b -> b.bind(ThreadPool.class).toInstance(threadPool)); + ActionModule actionModule = new ActionModule(false, true, settings, null, settingsModule.getClusterSettings(), + pluginsService.filterPlugins(ActionPlugin.class)); + modules.add(actionModule); + + pluginsService.processModules(modules); + CircuitBreakerService circuitBreakerService = Node.createCircuitBreakerService(settingsModule.getSettings(), + settingsModule.getClusterSettings()); + resourcesToClose.add(circuitBreakerService); + BigArrays bigArrays = new BigArrays(settings, circuitBreakerService); + resourcesToClose.add(bigArrays); + modules.add(settingsModule); + modules.add((b -> { + b.bind(BigArrays.class).toInstance(bigArrays); + b.bind(PluginsService.class).toInstance(pluginsService); + b.bind(CircuitBreakerService.class).toInstance(circuitBreakerService); + b.bind(NamedWriteableRegistry.class).toInstance(namedWriteableRegistry); + })); + Injector injector = modules.createInjector(); + final TransportService transportService = injector.getInstance(TransportService.class); + final ProxyActionMap proxy = new ProxyActionMap(settings, transportService, + actionModule.getActions().values().stream() + .map(ActionPlugin.ActionHandler::getAction).collect(Collectors.toList())); + List pluginLifecycleComponents = new ArrayList<>(); + pluginLifecycleComponents.addAll(pluginsService.getGuiceServiceClasses().stream() + .map(injector::getInstance).collect(Collectors.toList())); + resourcesToClose.addAll(pluginLifecycleComponents); + transportService.start(); + transportService.acceptIncomingRequests(); + ClientTemplate transportClient = new ClientTemplate(injector, pluginLifecycleComponents, + proxy, namedWriteableRegistry); + resourcesToClose.clear(); + return transportClient; + } finally { + IOUtils.closeWhileHandlingException(resourcesToClose); } } - /** - * The {@link ProxyActionMap} must be declared public. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - public static class ProxyActionMap { + private static final Logger logger = LogManager.getLogger(TransportClient.class); - private final ImmutableMap proxies; + private static PluginsService newPluginService(final Settings settings, Collection> plugins) { + final Settings.Builder settingsBuilder = Settings.builder() + .put(TcpTransport.PING_SCHEDULE.getKey(), "5s") // enable by default the transport schedule ping interval + .put(NetworkService.NETWORK_SERVER.getKey(), false) + .put(CLIENT_TYPE_SETTING_S.getKey(), CLIENT_TYPE); + if (!settings.isEmpty()) { + logger.info(settings.getAsMap()); + settingsBuilder.put(InternalSettingsPreparer.prepareSettings(settings)); + } + return new PluginsService(settingsBuilder.build(), null, null, plugins); + } - @Inject - public ProxyActionMap(Settings settings, TransportService transportService, Map actions) { - MapBuilder actionsBuilder = new MapBuilder<>(); - for (GenericAction action : actions.values()) { - if (action instanceof Action) { - actionsBuilder.put((Action) action, new TransportActionNodeProxy(settings, action, transportService)); - } - } - this.proxies = actionsBuilder.immutableMap(); + private static final class ClientTemplate { + final Injector injector; + private final List pluginLifecycleComponents; + private final ProxyActionMap proxy; + private final NamedWriteableRegistry namedWriteableRegistry; + + private ClientTemplate(Injector injector, List pluginLifecycleComponents, + ProxyActionMap proxy, NamedWriteableRegistry namedWriteableRegistry) { + this.injector = injector; + this.pluginLifecycleComponents = pluginLifecycleComponents; + this.proxy = proxy; + this.namedWriteableRegistry = namedWriteableRegistry; } - public ImmutableMap getProxies() { - return proxies; + Settings getSettings() { + return injector.getInstance(Settings.class); } - } + ThreadPool getThreadPool() { + return injector.getInstance(ThreadPool.class); + } + } } From 1888b3630fe2b2577017fb678eeda961e01a6fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sat, 19 Nov 2016 16:43:57 +0100 Subject: [PATCH 02/11] align tests, loggers, sonarqube issues --- build.gradle | 10 ++- .../org/xbib/elasticsearch/NodeTestBase.java | 2 +- .../{ => extras/client}/AliasTest.java | 9 +- ...erBlockTest.java => ClusterBlockTest.java} | 10 +-- .../{ => extras/client}/SearchTest.java | 3 +- .../{ => extras/client}/SimpleTest.java | 6 +- .../{ => extras/client}/WildcardTest.java | 3 +- .../client/node/BulkNodeClientTest.java | 11 +-- .../client/node/BulkNodeDuplicateIDTest.java | 8 +- .../client/node/BulkNodeIndexAliasTest.java | 4 +- .../client/node/BulkNodeReplicaTest.java | 13 +-- .../node/BulkNodeUpdateReplicaLevelTest.java | 6 +- .../transport/BulkTransportClientTest.java | 55 +++++++++++-- .../BulkTransportDuplicateIDTest.java | 16 ++-- .../transport/BulkTransportReplicaTest.java | 19 +++-- .../BulkTransportUpdateReplicaLevelTest.java | 6 +- .../extras/client/transport/package-info.java | 4 + .../org/xbib/elasticsearch/package-info.java | 2 +- .../java/suites/ListenerSuite.java | 3 + .../java/suites/MiscTestSuite.java | 8 +- .../java/suites/package-info.java | 4 + .../extras/client/AbstractClient.java | 4 +- .../extras/client/node/BulkNodeClient.java | 3 + .../client/transport/BulkTransportClient.java | 8 +- .../client/transport/TransportClient.java | 82 ++++++++++--------- 25 files changed, 193 insertions(+), 106 deletions(-) rename src/integration-test/java/org/xbib/elasticsearch/{ => extras/client}/AliasTest.java (95%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/{node/BulkNodeClusterBlockTest.java => ClusterBlockTest.java} (88%) rename src/integration-test/java/org/xbib/elasticsearch/{ => extras/client}/SearchTest.java (96%) rename src/integration-test/java/org/xbib/elasticsearch/{ => extras/client}/SimpleTest.java (94%) rename src/integration-test/java/org/xbib/elasticsearch/{ => extras/client}/WildcardTest.java (97%) create mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java create mode 100644 src/integration-test/java/suites/package-info.java diff --git a/build.gradle b/build.gradle index 33d34f8..95c2701 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'org.xbib' -version = '5.0.1.0' +version = '5.0.1.1' printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", @@ -56,7 +56,13 @@ configurations { dependencies { compile "org.xbib:metrics:1.0.0" - compile "org.elasticsearch.client:transport:5.0.1" + compile("org.elasticsearch.client:transport:5.0.1") { + exclude group: 'org.elasticsearch', module: 'securesm' + exclude group: 'org.elasticsearch.plugin', module: 'transport-netty3-client' + exclude group: 'org.elasticsearch.plugin', module: 'reindex-client' + exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' + exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' + } compile "org.apache.logging.log4j:log4j-api:2.7" testCompile "junit:junit:4.12" testCompile "org.apache.logging.log4j:log4j-core:2.7" diff --git a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java index f68a729..86bb6dc 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java +++ b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java @@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class NodeTestBase { - protected static final Logger logger = LogManager.getLogger("test"); + private static final Logger logger = LogManager.getLogger("test"); private static final Random random = new Random(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java similarity index 95% rename from src/integration-test/java/org/xbib/elasticsearch/AliasTest.java rename to src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java index 0c683c1..81a84d9 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/AliasTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch; +package org.xbib.elasticsearch.extras.client; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -14,6 +14,7 @@ import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.common.Strings; import org.junit.Test; +import org.xbib.elasticsearch.NodeTestBase; import java.io.IOException; import java.util.Collections; @@ -72,10 +73,8 @@ public class AliasTest extends NodeTestBase { Set result = new TreeSet<>(Collections.reverseOrder()); for (ObjectCursor indexName : getAliasesResponse.getAliases().keys()) { Matcher m = pattern.matcher(indexName.value); - if (m.matches()) { - if (alias.equals(m.group(1))) { - result.add(indexName.value); - } + if (m.matches() && alias.equals(m.group(1))) { + result.add(indexName.value); } } Iterator it = result.iterator(); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java similarity index 88% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java rename to src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java index 2b79ddc..07e492f 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClusterBlockTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java @@ -1,4 +1,6 @@ -package org.xbib.elasticsearch.extras.client.node; +package org.xbib.elasticsearch.extras.client; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -11,14 +13,12 @@ import org.junit.Before; import org.junit.Test; import org.xbib.elasticsearch.NodeTestBase; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; - /** * */ -public class BulkNodeClusterBlockTest extends NodeTestBase { +public class ClusterBlockTest extends NodeTestBase { - private static final Logger logger = LogManager.getLogger(BulkNodeClusterBlockTest.class.getName()); + private static final Logger logger = LogManager.getLogger(ClusterBlockTest.class.getName()); @Before public void startNodes() { diff --git a/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java similarity index 96% rename from src/integration-test/java/org/xbib/elasticsearch/SearchTest.java rename to src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java index efed10c..93a7ca7 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/SearchTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch; +package org.xbib.elasticsearch.extras.client; import static org.elasticsearch.client.Requests.indexRequest; import static org.elasticsearch.client.Requests.refreshRequest; @@ -15,6 +15,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortOrder; import org.junit.Test; +import org.xbib.elasticsearch.NodeTestBase; /** * diff --git a/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java similarity index 94% rename from src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java rename to src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java index 70f1373..f32e321 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/SimpleTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch; +package org.xbib.elasticsearch.extras.client; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; @@ -13,6 +13,7 @@ import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Settings; import org.junit.Test; +import org.xbib.elasticsearch.NodeTestBase; /** * @@ -39,7 +40,8 @@ public class SimpleTest extends NodeTestBase { } catch (Exception e) { // ignore } - CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client("1"), CreateIndexAction.INSTANCE) + CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client("1"), + CreateIndexAction.INSTANCE) .setIndex("test") .setSettings(Settings.builder() .put("index.analysis.analyzer.default.filter.0", "lowercase") diff --git a/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java similarity index 97% rename from src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java rename to src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java index d412654..327b619 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/WildcardTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch; +package org.xbib.elasticsearch.extras.client; import static org.elasticsearch.client.Requests.indexRequest; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; @@ -9,6 +9,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.query.QueryBuilder; import org.junit.Test; +import org.xbib.elasticsearch.NodeTestBase; import java.io.IOException; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java index a1165ff..4a295a7 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java @@ -6,14 +6,12 @@ import static org.junit.Assert.assertFalse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.ExecutorServices; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -28,7 +26,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** @@ -68,7 +65,7 @@ public class BulkNodeClientTest extends NodeTestBase { } @Test - public void testMappingNodeClient() throws Exception { + public void testBulkNodeClientMapping() throws Exception { final BulkNodeClient client = ClientBuilder.builder() .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) .setMetric(new SimpleBulkMetric()) @@ -98,7 +95,7 @@ public class BulkNodeClientTest extends NodeTestBase { } @Test - public void testSingleDocNodeClient() { + public void testBulkNodeClientSingleDoc() { final BulkNodeClient client = ClientBuilder.builder() .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(30)) @@ -127,7 +124,7 @@ public class BulkNodeClientTest extends NodeTestBase { } @Test - public void testRandomDocsNodeClient() throws Exception { + public void testBulkNodeClientRandomDocs() throws Exception { long numactions = NUM_ACTIONS; final BulkNodeClient client = ClientBuilder.builder() .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) @@ -155,7 +152,7 @@ public class BulkNodeClientTest extends NodeTestBase { } @Test - public void testThreadedRandomDocsNodeClient() throws Exception { + public void testBulkNodeClientThreadedRandomDocs() throws Exception { int maxthreads = Runtime.getRuntime().availableProcessors(); Long maxactions = MAX_ACTIONS; final Long maxloop = NUM_ACTIONS; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java index d7e9a30..cb32cac 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java @@ -1,5 +1,10 @@ package org.xbib.elasticsearch.extras.client.node; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.search.SearchAction; @@ -12,9 +17,6 @@ import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.*; - /** * */ diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java index a6f00a4..1c3b3fd 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java @@ -1,5 +1,7 @@ package org.xbib.elasticsearch.extras.client.node; +import static org.junit.Assert.assertFalse; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder; @@ -17,8 +19,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.junit.Assert.assertFalse; - /** * */ diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java index 637a28e..b0d8100 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java @@ -1,5 +1,9 @@ package org.xbib.elasticsearch.extras.client.node; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.stats.CommonStats; @@ -22,16 +26,12 @@ import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; import java.util.Map; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - /** * */ public class BulkNodeReplicaTest extends NodeTestBase { - private final static Logger logger = LogManager.getLogger(BulkNodeReplicaTest.class.getName()); + private static final Logger logger = LogManager.getLogger(BulkNodeReplicaTest.class.getName()); @Test public void testReplicaLevel() throws Exception { @@ -80,7 +80,8 @@ public class BulkNodeReplicaTest extends NodeTestBase { long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); logger.info("query total hits={}", hits); assertEquals(2468, hits); - IndicesStatsRequestBuilder indicesStatsRequestBuilder = new IndicesStatsRequestBuilder(client.client(), IndicesStatsAction.INSTANCE) + IndicesStatsRequestBuilder indicesStatsRequestBuilder = new IndicesStatsRequestBuilder(client.client(), + IndicesStatsAction.INSTANCE) .all(); IndicesStatsResponse response = indicesStatsRequestBuilder.execute().actionGet(); for (Map.Entry m : response.getIndices().entrySet()) { diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java index 7f43457..6d5cbe1 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java @@ -1,5 +1,8 @@ package org.xbib.elasticsearch.extras.client.node; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.client.transport.NoNodeAvailableException; @@ -11,9 +14,6 @@ import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - /** * */ diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java index 4b6dc41..205aa88 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java @@ -1,10 +1,20 @@ package org.xbib.elasticsearch.extras.client.transport; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.junit.Before; import org.junit.Test; @@ -12,6 +22,7 @@ import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.xbib.elasticsearch.extras.client.node.BulkNodeClient; import java.io.IOException; import java.util.concurrent.CountDownLatch; @@ -20,14 +31,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - /** * */ public class BulkTransportClientTest extends NodeTestBase { + private static final Logger logger = LogManager.getLogger(BulkTransportClientTest.class.getName()); + private static final Long MAX_ACTIONS = 1000L; private static final Long NUM_ACTIONS = 1234L; @@ -43,7 +53,7 @@ public class BulkTransportClientTest extends NodeTestBase { } @Test - public void testBulkClientIndexCreation() throws IOException { + public void testBulkTransportClientNewIndex() throws IOException { logger.info("firing up BulkTransportClient"); final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) @@ -75,7 +85,38 @@ public class BulkTransportClientTest extends NodeTestBase { } @Test - public void testSingleDocBulkClient() throws IOException { + public void testBulkTransportClientMapping() throws Exception { + final BulkTransportClient client = ClientBuilder.builder() + .put(getClientSettings()) + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .toBulkTransportClient(); + XContentBuilder builder = jsonBuilder() + .startObject() + .startObject("test") + .startObject("properties") + .startObject("location") + .field("type", "geo_point") + .endObject() + .endObject() + .endObject() + .endObject(); + client.mapping("test", builder.string()); + client.newIndex("test"); + GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices("test"); + GetMappingsResponse getMappingsResponse = + client.client().execute(GetMappingsAction.INSTANCE, getMappingsRequest).actionGet(); + logger.info("mappings={}", getMappingsResponse.getMappings()); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + client.shutdown(); + } + + @Test + public void testBulkTransportClientSingleDoc() throws IOException { logger.info("firing up BulkTransportClient"); final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) @@ -111,7 +152,7 @@ public class BulkTransportClientTest extends NodeTestBase { } @Test - public void testRandomDocsBulkClient() { + public void testBulkTransportClientRandomDocs() { long numactions = NUM_ACTIONS; final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) @@ -147,7 +188,7 @@ public class BulkTransportClientTest extends NodeTestBase { } @Test - public void testThreadedRandomDocsBulkClient() { + public void testBulkTransportClientThreadedRandomDocs() { int maxthreads = Runtime.getRuntime().availableProcessors(); long maxactions = MAX_ACTIONS; final long maxloop = NUM_ACTIONS; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java index 574928c..148215a 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java @@ -1,5 +1,12 @@ package org.xbib.elasticsearch.extras.client.transport; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; @@ -10,17 +17,16 @@ import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.*; - /** * */ public class BulkTransportDuplicateIDTest extends NodeTestBase { - private final static Long MAX_ACTIONS = 1000L; + private static final Logger logger = LogManager.getLogger(BulkTransportDuplicateIDTest.class.getName()); + + private static final Long MAX_ACTIONS = 1000L; - private final static Long NUM_ACTIONS = 12345L; + private static final Long NUM_ACTIONS = 12345L; @Test public void testDuplicateDocIDs() throws Exception { diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java index 386f212..ffad2c7 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java @@ -1,6 +1,17 @@ package org.xbib.elasticsearch.extras.client.transport; -import org.elasticsearch.action.admin.indices.stats.*; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.indices.stats.CommonStats; +import org.elasticsearch.action.admin.indices.stats.IndexShardStats; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequestBuilder; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; @@ -15,15 +26,13 @@ import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; import java.util.Map; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - /** * */ public class BulkTransportReplicaTest extends NodeTestBase { + private static final Logger logger = LogManager.getLogger(BulkTransportClientTest.class.getName()); + @Test public void testReplicaLevel() throws Exception { diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java index 331ac11..abcf7a5 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java @@ -1,5 +1,8 @@ package org.xbib.elasticsearch.extras.client.transport; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.client.transport.NoNodeAvailableException; @@ -11,9 +14,6 @@ import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - /** * */ diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java new file mode 100644 index 0000000..f55c996 --- /dev/null +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing extras for transport client. + */ +package org.xbib.elasticsearch.extras.client.transport; diff --git a/src/integration-test/java/org/xbib/elasticsearch/package-info.java b/src/integration-test/java/org/xbib/elasticsearch/package-info.java index 2958ce1..3c6e5c6 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/package-info.java +++ b/src/integration-test/java/org/xbib/elasticsearch/package-info.java @@ -1,4 +1,4 @@ /** * Test classes for testing Elasticsearch. */ -package org.xbib.elasticsearch; \ No newline at end of file +package org.xbib.elasticsearch; diff --git a/src/integration-test/java/suites/ListenerSuite.java b/src/integration-test/java/suites/ListenerSuite.java index c02d371..4101890 100644 --- a/src/integration-test/java/suites/ListenerSuite.java +++ b/src/integration-test/java/suites/ListenerSuite.java @@ -6,6 +6,9 @@ import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; +/** + * + */ public class ListenerSuite extends Suite { private final TestListener listener = new TestListener(); diff --git a/src/integration-test/java/suites/MiscTestSuite.java b/src/integration-test/java/suites/MiscTestSuite.java index ea23630..f832e88 100644 --- a/src/integration-test/java/suites/MiscTestSuite.java +++ b/src/integration-test/java/suites/MiscTestSuite.java @@ -2,10 +2,10 @@ package suites; import org.junit.runner.RunWith; import org.junit.runners.Suite; -import org.xbib.elasticsearch.AliasTest; -import org.xbib.elasticsearch.SearchTest; -import org.xbib.elasticsearch.SimpleTest; -import org.xbib.elasticsearch.WildcardTest; +import org.xbib.elasticsearch.extras.client.AliasTest; +import org.xbib.elasticsearch.extras.client.SearchTest; +import org.xbib.elasticsearch.extras.client.SimpleTest; +import org.xbib.elasticsearch.extras.client.WildcardTest; /** * diff --git a/src/integration-test/java/suites/package-info.java b/src/integration-test/java/suites/package-info.java new file mode 100644 index 0000000..a878170 --- /dev/null +++ b/src/integration-test/java/suites/package-info.java @@ -0,0 +1,4 @@ +/** + * Test suites. + */ +package suites; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java index f4ce3aa..ec25b81 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java @@ -1,9 +1,9 @@ package org.xbib.elasticsearch.extras.client; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java index 17a0779..bac6bca 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java @@ -101,6 +101,9 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { metric.start(); } BulkProcessor.Listener listener = new BulkProcessor.Listener() { + + private final Logger logger = LogManager.getLogger(BulkNodeClient.class.getName() + ".Listener"); + @Override public void beforeBulk(long executionId, BulkRequest request) { long l = -1; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java index be51e62..3d159ab 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java @@ -27,7 +27,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.env.Environment; import org.elasticsearch.transport.Netty4Plugin; import org.xbib.elasticsearch.extras.client.AbstractClient; import org.xbib.elasticsearch.extras.client.BulkControl; @@ -54,8 +53,6 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods private static final Logger logger = LogManager.getLogger(BulkTransportClient.class.getName()); - private static final Settings DEFAULT_SETTINGS = Settings.builder().put("transport.type.default", "local").build(); - private int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; private int maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS; @@ -95,6 +92,9 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods } resetSettings(); BulkProcessor.Listener listener = new BulkProcessor.Listener() { + + private final Logger logger = LogManager.getLogger(BulkTransportClient.class.getName() + ".Listener"); + @Override public void beforeBulk(long executionId, BulkRequest request) { long l = -1L; @@ -170,7 +170,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods builder.setBulkSize(maxVolumePerRequest); } this.bulkProcessor = builder.build(); - // aut-connect here + // auto-connect here try { Collection addrs = findAddresses(settings); if (!connect(addrs, settings.getAsBoolean("autodiscover", false))) { diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java index 2ded1ab..d0c377d 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java @@ -91,15 +91,14 @@ public class TransportClient extends AbstractClient { private final Object mutex = new Object(); - private volatile List listedNodes = Collections.emptyList(); - private volatile List nodes = Collections.emptyList(); + private volatile List listedNodes = Collections.emptyList(); + private volatile List filteredNodes = Collections.emptyList(); private volatile boolean closed; - /** * Creates a new TransportClient with the given settings and plugins. * @param settings settings @@ -197,37 +196,44 @@ public class TransportClient extends AbstractClient { return this; } + /** + * Adds a list of transport addresses that will be used to connect to. + * The Node this transport address represents will be used if its possible to connect to it. + * If it is unavailable, it will be automatically connected to once it is up. + * In order to get the list of all the current connected nodes, please see {@link #connectedNodes()}. + * + * @param transportAddresses transport addressses + * @return this transport client + */ public TransportClient addTransportAddresses(Collection transportAddresses) { synchronized (mutex) { if (closed) { throw new IllegalStateException("transport client is closed, can't add addresses"); } - List filtered = new ArrayList<>(transportAddresses.size()); - for (TransportAddress transportAddress : transportAddresses) { + Set discoveryNodeList = new HashSet<>(); + discoveryNodeList.addAll(listedNodes); + logger.debug("before adding: nodes={} listednodes={} transportAddresses={}", + nodes, listedNodes, transportAddresses); + for (TransportAddress newTransportAddress : transportAddresses) { boolean found = false; - for (DiscoveryNode otherNode : listedNodes) { - if (otherNode.getAddress().equals(transportAddress)) { + for (DiscoveryNode discoveryNode : discoveryNodeList) { + logger.debug("checking existing address [{}] against new [{}]", + discoveryNode.getAddress(), newTransportAddress); + if (discoveryNode.getAddress().sameHost(newTransportAddress)) { found = true; - logger.debug("address [{}] already exists with [{}], ignoring...", transportAddress, otherNode); + logger.debug("address [{}] already connected, ignoring", newTransportAddress, discoveryNode); break; } } if (!found) { - filtered.add(transportAddress); + DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeId.incrementAndGet(), + newTransportAddress, + Version.CURRENT.minimumCompatibilityVersion()); + logger.debug("adding address [{}]", node); + discoveryNodeList.add(node); } } - if (filtered.isEmpty()) { - return this; - } - List discoveryNodeList = new ArrayList<>(); - discoveryNodeList.addAll(listedNodes()); - for (TransportAddress transportAddress : filtered) { - DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeId.incrementAndGet(), transportAddress, - Version.CURRENT.minimumCompatibilityVersion()); - logger.debug("adding address [{}]", node); - discoveryNodeList.add(node); - } - listedNodes = Collections.unmodifiableList(discoveryNodeList); + listedNodes = Collections.unmodifiableList(new ArrayList<>(discoveryNodeList)); connect(); } return this; @@ -265,13 +271,16 @@ public class TransportClient extends AbstractClient { return; } closed = true; + logger.debug("disconnecting from nodes {}", nodes); for (DiscoveryNode node : nodes) { transportService.disconnectFromNode(node); } + nodes = Collections.emptyList(); + logger.debug("disconnecting from listed nodes {}", listedNodes); for (DiscoveryNode listedNode : listedNodes) { transportService.disconnectFromNode(listedNode); } - nodes = Collections.emptyList(); + listedNodes = Collections.emptyList(); } injector.getInstance(TransportService.class).close(); for (Class plugin : injector.getInstance(PluginsService.class).getGuiceServiceClasses()) { @@ -290,7 +299,7 @@ public class TransportClient extends AbstractClient { for (DiscoveryNode listedNode : listedNodes) { if (!transportService.nodeConnected(listedNode)) { try { - logger.trace("connecting to listed node (light) [{}]", listedNode); + logger.debug("connecting to listed node (light) [{}]", listedNode); transportService.connectToNodeLight(listedNode); } catch (Exception e) { logger.debug("failed to connect to node [{}], removed from nodes list", e, listedNode); @@ -309,7 +318,7 @@ public class TransportClient extends AbstractClient { } }).txGet(); if (!clusterName.equals(livenessResponse.getClusterName())) { - logger.warn("node {} not part of the cluster {}, ignoring...", listedNode, clusterName); + logger.warn("node {} not part of the cluster {}, ignoring", listedNode, clusterName); newFilteredNodes.add(listedNode); } else if (livenessResponse.getDiscoveryNode() != null) { DiscoveryNode nodeWithInfo = livenessResponse.getDiscoveryNode(); @@ -323,7 +332,7 @@ public class TransportClient extends AbstractClient { newNodes.add(listedNode); } } catch (Exception e) { - logger.info("failed to get node info for {}, disconnecting...", e, listedNode); + logger.info("failed to get node info for {}, disconnecting", e, listedNode); transportService.disconnectFromNode(listedNode); } } @@ -331,7 +340,7 @@ public class TransportClient extends AbstractClient { DiscoveryNode node = it.next(); if (!transportService.nodeConnected(node)) { try { - logger.trace("connecting to node [{}]", node); + logger.debug("connecting to new node [{}]", node); transportService.connectToNode(node); } catch (Exception e) { it.remove(); @@ -340,6 +349,7 @@ public class TransportClient extends AbstractClient { } } this.nodes = Collections.unmodifiableList(new ArrayList<>(newNodes)); + logger.debug("connected to {} nodes", nodes.size()); this.filteredNodes = Collections.unmodifiableList(new ArrayList<>(newFilteredNodes)); } @@ -392,10 +402,13 @@ public class TransportClient extends AbstractClient { } - private static ClientTemplate buildTemplate(Settings providedSettings, Settings defaultSettings, + private static ClientTemplate buildTemplate(Settings givenSettings, Settings defaultSettings, Collection> plugins) { + Settings providedSettings = givenSettings; if (!Node.NODE_NAME_SETTING.exists(providedSettings)) { - providedSettings = Settings.builder().put(providedSettings).put(Node.NODE_NAME_SETTING.getKey(), "_client_").build(); + providedSettings = Settings.builder().put(providedSettings) + .put(Node.NODE_NAME_SETTING.getKey(), "_client_") + .build(); } final PluginsService pluginsService = newPluginService(providedSettings, plugins); final Settings settings = Settings.builder().put(defaultSettings).put(pluginsService.updatedSettings()).build(); @@ -423,7 +436,7 @@ public class TransportClient extends AbstractClient { NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(entries); ModulesBuilder modules = new ModulesBuilder(); - // plugin modules must be added here, before others or we can get crazy injection errors... + // plugin modules must be added here, before others or we can get crazy injection errors for (Module pluginModule : pluginsService.createGuiceModules()) { modules.add(pluginModule); } @@ -457,8 +470,7 @@ public class TransportClient extends AbstractClient { resourcesToClose.addAll(pluginLifecycleComponents); transportService.start(); transportService.acceptIncomingRequests(); - ClientTemplate transportClient = new ClientTemplate(injector, pluginLifecycleComponents, - proxy, namedWriteableRegistry); + ClientTemplate transportClient = new ClientTemplate(injector, proxy); resourcesToClose.clear(); return transportClient; } finally { @@ -482,16 +494,12 @@ public class TransportClient extends AbstractClient { private static final class ClientTemplate { final Injector injector; - private final List pluginLifecycleComponents; private final ProxyActionMap proxy; - private final NamedWriteableRegistry namedWriteableRegistry; - private ClientTemplate(Injector injector, List pluginLifecycleComponents, - ProxyActionMap proxy, NamedWriteableRegistry namedWriteableRegistry) { + private ClientTemplate(Injector injector, + ProxyActionMap proxy) { this.injector = injector; - this.pluginLifecycleComponents = pluginLifecycleComponents; this.proxy = proxy; - this.namedWriteableRegistry = namedWriteableRegistry; } Settings getSettings() { From 6bd2f65a666a4112ae710556a9edd87f4fd06b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Mon, 12 Dec 2016 11:51:35 +0100 Subject: [PATCH 03/11] update to Elasticsearch 5.1.1, Gradle 3.2.1 --- build.gradle | 16 ++++---- gradle.properties | 3 ++ gradle/ext.gradle | 8 ---- gradle/wrapper/gradle-wrapper.jar | Bin 52928 -> 54227 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 19 +++++---- settings.gradle | 2 +- .../org/xbib/elasticsearch/NodeTestBase.java | 6 ++- .../extras/client/SimpleTest.java | 21 +++------- .../extras/client/WildcardTest.java | 37 ++++++------------ .../extras/client/BulkProcessor.java | 6 +-- .../client/transport/TransportClient.java | 28 ++++++++----- 12 files changed, 72 insertions(+), 78 deletions(-) create mode 100644 gradle.properties delete mode 100644 gradle/ext.gradle diff --git a/build.gradle b/build.gradle index 95c2701..0c69d48 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,9 @@ plugins { id "org.sonarqube" version "2.2" - id "org.ajoberstar.github-pages" version "1.6.0-rc.1" id "org.xbib.gradle.plugin.jbake" version "1.2.1" } -group = 'org.xbib' -version = '5.0.1.1' - printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", InetAddress.getLocalHost(), @@ -28,9 +24,15 @@ apply plugin: 'findbugs' apply plugin: 'pmd' apply plugin: 'checkstyle' apply plugin: "jacoco" -apply plugin: 'org.ajoberstar.github-pages' -apply from: 'gradle/ext.gradle' +ext { + user = 'xbib' + name = 'elasticsearch-extras-client' + description = 'Some extras implemented for using Elasticsearch clients (node and transport)' + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' +} sourceSets { integrationTest { @@ -56,7 +58,7 @@ configurations { dependencies { compile "org.xbib:metrics:1.0.0" - compile("org.elasticsearch.client:transport:5.0.1") { + compile("org.elasticsearch.client:transport:5.1.1") { exclude group: 'org.elasticsearch', module: 'securesm' exclude group: 'org.elasticsearch.plugin', module: 'transport-netty3-client' exclude group: 'org.elasticsearch.plugin', module: 'reindex-client' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..60e7e76 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +group = org.xbib +name = elasticsearch-extras-client +version = 5.1.1.0 diff --git a/gradle/ext.gradle b/gradle/ext.gradle deleted file mode 100644 index 7bb7c73..0000000 --- a/gradle/ext.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - user = 'xbib' - name = 'elasticsearch-extras-client' - description = 'Some extras implemented for using Elasticsearch clients (node and transport)' - scmUrl = 'https://github.com/' + user + '/' + name - scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa237849ef3607e39c3b334a92a65367962071..51288f9c2f05faf8d42e1a751a387ca7923882c3 100644 GIT binary patch delta 15239 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 delta 13979 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2a06e59..d56c0fe 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 01 14:46:00 CET 2016 +#Sun Dec 04 10:06:25 CET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-all.zip diff --git a/gradlew b/gradlew index 9aa616c..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -154,16 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/settings.gradle b/settings.gradle index ef50653..89f4110 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'elasticsearch-extras-client' +rootProject.name = name diff --git a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java index 86bb6dc..618fa06 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java +++ b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java @@ -22,7 +22,11 @@ import org.junit.Before; import org.xbib.elasticsearch.extras.client.NetworkUtils; import java.io.IOException; -import java.nio.file.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.Map; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java index f32e321..c3fe074 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.junit.Test; import org.xbib.elasticsearch.NodeTestBase; @@ -20,27 +21,17 @@ import org.xbib.elasticsearch.NodeTestBase; */ public class SimpleTest extends NodeTestBase { - protected Settings getNodeSettings() { - return Settings.builder() - .put("cluster.name", getClusterName()) - .put("discovery.type", "local") - .put("transport.type", "local") - .put("http.enabled", false) - .put("path.home", getHome()) - .put("node.max_local_storage_nodes", 5) - .build(); - } - @Test public void test() throws Exception { + Client client = client("1"); try { DeleteIndexRequestBuilder deleteIndexRequestBuilder = - new DeleteIndexRequestBuilder(client("1"), DeleteIndexAction.INSTANCE, "test"); + new DeleteIndexRequestBuilder(client, DeleteIndexAction.INSTANCE, "test"); deleteIndexRequestBuilder.execute().actionGet(); } catch (Exception e) { // ignore } - CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client("1"), + CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client, CreateIndexAction.INSTANCE) .setIndex("test") .setSettings(Settings.builder() @@ -50,7 +41,7 @@ public class SimpleTest extends NodeTestBase { .build()); createIndexRequestBuilder.execute().actionGet(); - IndexRequestBuilder indexRequestBuilder = new IndexRequestBuilder(client("1"), IndexAction.INSTANCE); + IndexRequestBuilder indexRequestBuilder = new IndexRequestBuilder(client, IndexAction.INSTANCE); indexRequestBuilder .setIndex("test") .setType("test") @@ -60,7 +51,7 @@ public class SimpleTest extends NodeTestBase { .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .execute() .actionGet(); - String doc = client("1").prepareSearch("test") + String doc = client.prepareSearch("test") .setTypes("test") .setQuery(matchQuery("field", "1%2fPJJP3JV2C24iDfEu9XpHBaYxXh%2fdHTbmchB35SDznXO2g8Vz4D7GTIvY54iMiX_149c95f02a8")) diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java index 327b619..9b16eda 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java @@ -6,7 +6,6 @@ import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.Client; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.query.QueryBuilder; import org.junit.Test; import org.xbib.elasticsearch.NodeTestBase; @@ -18,33 +17,23 @@ import java.io.IOException; */ public class WildcardTest extends NodeTestBase { - protected Settings getNodeSettings() { - return Settings.builder() - .put("cluster.name", getClusterName()) - .put("discovery.type", "local") - .put("transport.type", "local") - .put("http.enabled", false) - .put("path.home", getHome()) - .put("node.max_local_storage_nodes", 5) - .build(); - } - @Test public void testWildcard() throws Exception { - index(client("1"), "1", "010"); - index(client("1"), "2", "0*0"); + Client client = client("1"); + index(client, "1", "010"); + index(client, "2", "0*0"); // exact - validateCount(client("1"), queryStringQuery("010").defaultField("field"), 1); - validateCount(client("1"), queryStringQuery("0\\*0").defaultField("field"), 1); + validateCount(client, queryStringQuery("010").defaultField("field"), 1); + validateCount(client, queryStringQuery("0\\*0").defaultField("field"), 1); // pattern - validateCount(client("1"), queryStringQuery("0*0").defaultField("field"), 1); // 2? - validateCount(client("1"), queryStringQuery("0?0").defaultField("field"), 1); // 2? - validateCount(client("1"), queryStringQuery("0**0").defaultField("field"), 1); // 2? - validateCount(client("1"), queryStringQuery("0??0").defaultField("field"), 0); - validateCount(client("1"), queryStringQuery("*10").defaultField("field"), 1); - validateCount(client("1"), queryStringQuery("*1*").defaultField("field"), 1); - validateCount(client("1"), queryStringQuery("*\\*0").defaultField("field"), 0); // 1? - validateCount(client("1"), queryStringQuery("*\\**").defaultField("field"), 0); // 1? + validateCount(client, queryStringQuery("0*0").defaultField("field"), 1); // 2? + validateCount(client, queryStringQuery("0?0").defaultField("field"), 1); // 2? + validateCount(client, queryStringQuery("0**0").defaultField("field"), 1); // 2? + validateCount(client, queryStringQuery("0??0").defaultField("field"), 0); + validateCount(client, queryStringQuery("*10").defaultField("field"), 1); + validateCount(client, queryStringQuery("*1*").defaultField("field"), 1); + validateCount(client, queryStringQuery("*\\*0").defaultField("field"), 0); // 1? + validateCount(client, queryStringQuery("*\\**").defaultField("field"), 0); // 1? } private void index(Client client, String id, String fieldValue) throws IOException { diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java index b15c243..56a72e5 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java @@ -147,7 +147,7 @@ public class BulkProcessor implements Closeable { * @param request request * @return his bulk processor */ - public BulkProcessor add(ActionRequest request) { + public BulkProcessor add(ActionRequest request) { return add(request, null); } @@ -158,7 +158,7 @@ public class BulkProcessor implements Closeable { * @param payload payload * @return his bulk processor */ - public BulkProcessor add(ActionRequest request, @Nullable Object payload) { + public BulkProcessor add(ActionRequest request, @Nullable Object payload) { internalAdd(request, payload); return this; } @@ -169,7 +169,7 @@ public class BulkProcessor implements Closeable { } } - private synchronized void internalAdd(ActionRequest request, @Nullable Object payload) { + private synchronized void internalAdd(ActionRequest request, @Nullable Object payload) { ensureOpen(); bulkRequest.add(request, payload); executeIfNeeded(); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java index d0c377d..fc83ae0 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java @@ -40,6 +40,7 @@ import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.node.Node; import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.NetworkPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.plugins.SearchPlugin; @@ -48,6 +49,7 @@ import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.FutureTransportResponseHandler; import org.elasticsearch.transport.TcpTransport; +import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; @@ -355,7 +357,7 @@ public class TransportClient extends AbstractClient { @Override @SuppressWarnings({"unchecked", "rawtypes"}) - protected , S extends ActionResponse, T extends ActionRequestBuilder> + protected > void doExecute(Action action, final R request, final ActionListener listener) { final TransportActionNodeProxy proxyAction = proxy.getProxies().get(action); if (proxyAction == null) { @@ -425,10 +427,11 @@ public class TransportClient extends AbstractClient { additionalSettings.addAll(builder.getRegisteredSettings()); } SettingsModule settingsModule = new SettingsModule(settings, additionalSettings, additionalSettingsFilter); - NetworkModule networkModule = new NetworkModule(networkService, settings, true); - SearchModule searchModule = new SearchModule(settings, true, pluginsService.filterPlugins(SearchPlugin.class)); + + SearchModule searchModule = new SearchModule(settings, true, + pluginsService.filterPlugins(SearchPlugin.class)); List entries = new ArrayList<>(); - entries.addAll(networkModule.getNamedWriteables()); + entries.addAll(NetworkModule.getNamedWriteables()); entries.addAll(searchModule.getNamedWriteables()); entries.addAll(pluginsService.filterPlugins(Plugin.class).stream() .flatMap(p -> p.getNamedWriteables().stream()) @@ -440,27 +443,34 @@ public class TransportClient extends AbstractClient { for (Module pluginModule : pluginsService.createGuiceModules()) { modules.add(pluginModule); } - modules.add(networkModule); modules.add(b -> b.bind(ThreadPool.class).toInstance(threadPool)); - ActionModule actionModule = new ActionModule(false, true, settings, null, settingsModule.getClusterSettings(), + ActionModule actionModule = new ActionModule(false, true, + settings, null, settingsModule.getClusterSettings(), pluginsService.filterPlugins(ActionPlugin.class)); modules.add(actionModule); - pluginsService.processModules(modules); CircuitBreakerService circuitBreakerService = Node.createCircuitBreakerService(settingsModule.getSettings(), settingsModule.getClusterSettings()); - resourcesToClose.add(circuitBreakerService); BigArrays bigArrays = new BigArrays(settings, circuitBreakerService); + resourcesToClose.add(circuitBreakerService); resourcesToClose.add(bigArrays); modules.add(settingsModule); + NetworkModule networkModule = new NetworkModule(settings, true, + pluginsService.filterPlugins(NetworkPlugin.class), threadPool, + bigArrays, circuitBreakerService, namedWriteableRegistry, networkService); + final Transport transport = networkModule.getTransportSupplier().get(); + final TransportService transportService = new TransportService(settings, transport, threadPool, + networkModule.getTransportInterceptor(), null); modules.add((b -> { b.bind(BigArrays.class).toInstance(bigArrays); b.bind(PluginsService.class).toInstance(pluginsService); b.bind(CircuitBreakerService.class).toInstance(circuitBreakerService); b.bind(NamedWriteableRegistry.class).toInstance(namedWriteableRegistry); + b.bind(Transport.class).toInstance(transport); + b.bind(TransportService.class).toInstance(transportService); + b.bind(NetworkService.class).toInstance(networkService); })); Injector injector = modules.createInjector(); - final TransportService transportService = injector.getInstance(TransportService.class); final ProxyActionMap proxy = new ProxyActionMap(settings, transportService, actionModule.getActions().values().stream() .map(ActionPlugin.ActionHandler::getAction).collect(Collectors.toList())); From 4b563b9fce320fe94a65a550c948bd4e071ecccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Tue, 3 Jan 2017 09:28:17 +0100 Subject: [PATCH 04/11] remove jbake --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c69d48..9af88e1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,6 @@ plugins { id "org.sonarqube" version "2.2" - id "org.xbib.gradle.plugin.jbake" version "1.2.1" } printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + From 7b18b6740ec0602e214138d2c6b47d02ef6e2ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 12 Jan 2017 12:53:25 +0100 Subject: [PATCH 05/11] add native epoll for Linux --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 9af88e1..ba1f71c 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,8 @@ dependencies { exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' } + compile "io.netty:netty-transport-native-epoll:4.1.6.Final" + compile "io.netty:netty-transport-native-epoll:4.1.6.Final:linux-x86_64" compile "org.apache.logging.log4j:log4j-api:2.7" testCompile "junit:junit:4.12" testCompile "org.apache.logging.log4j:log4j-core:2.7" From 6f6dd8ae0864ad86253715e91385e0a6ae47ba64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 12 Jan 2017 13:14:12 +0100 Subject: [PATCH 06/11] add Maven Central repository --- build.gradle | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index ba1f71c..9e3e4ed 100644 --- a/build.gradle +++ b/build.gradle @@ -46,8 +46,9 @@ sourceSets { } } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +repositories { + mavenCentral() +} configurations { wagon @@ -64,7 +65,6 @@ dependencies { exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' } - compile "io.netty:netty-transport-native-epoll:4.1.6.Final" compile "io.netty:netty-transport-native-epoll:4.1.6.Final:linux-x86_64" compile "org.apache.logging.log4j:log4j-api:2.7" testCompile "junit:junit:4.12" @@ -72,6 +72,9 @@ dependencies { wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' } +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:all" << "-profile" << "compact3" From f1818719487c703fe06876a798b43c6124a1e6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sat, 1 Apr 2017 12:48:43 +0200 Subject: [PATCH 07/11] update to Elasticsearch 5.2.2 --- build.gradle | 14 ++++---- gradle.properties | 9 ++++- .../client/transport/TransportClient.java | 33 +++++++++++-------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index 9e3e4ed..87b3d58 100644 --- a/build.gradle +++ b/build.gradle @@ -57,19 +57,19 @@ configurations { } dependencies { - compile "org.xbib:metrics:1.0.0" - compile("org.elasticsearch.client:transport:5.1.1") { + compile("org.elasticsearch.client:transport:${project.property('elasticsearch-client-transport.version')}") { exclude group: 'org.elasticsearch', module: 'securesm' exclude group: 'org.elasticsearch.plugin', module: 'transport-netty3-client' exclude group: 'org.elasticsearch.plugin', module: 'reindex-client' exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' } - compile "io.netty:netty-transport-native-epoll:4.1.6.Final:linux-x86_64" - compile "org.apache.logging.log4j:log4j-api:2.7" - testCompile "junit:junit:4.12" - testCompile "org.apache.logging.log4j:log4j-core:2.7" - wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' + compile "org.xbib:metrics:${project.property('xbib-metrics.version')}" + compile "io.netty:netty-transport-native-epoll:${project.property('netty-transport-native-epoll.version')}:linux-x86_64" + compile "org.apache.logging.log4j:log4j-api:${project.property('log4j.version')}" + testCompile "junit:junit:${project.property('junit.version')}" + testCompile "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + wagon "org.apache.maven.wagon:wagon-ssh-external:${project.property('wagon.version')}" } sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/gradle.properties b/gradle.properties index 60e7e76..2a6c341 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,10 @@ group = org.xbib name = elasticsearch-extras-client -version = 5.1.1.0 +version = 5.2.2.0 + +elasticsearch-client-transport.version = 5.2.2 +xbib-metrics.version = 1.0.0 +netty-transport-native-epoll.version = 4.1.6.Final +log4j.version = 2.8 +junit.version = 4.12 +wagon.version = 2.10 diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java index fc83ae0..c639715 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java @@ -1,5 +1,6 @@ package org.xbib.elasticsearch.extras.client.transport; +import static java.util.stream.Collectors.toList; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; import org.apache.logging.log4j.LogManager; @@ -36,6 +37,7 @@ import org.elasticsearch.common.settings.SettingsModule; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.node.Node; import org.elasticsearch.node.internal.InternalSettingsPreparer; @@ -64,7 +66,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; +import java.util.function.Function; +import java.util.stream.Stream; /** * Stripped-down transport client without node sampling and without retrying. @@ -301,10 +304,10 @@ public class TransportClient extends AbstractClient { for (DiscoveryNode listedNode : listedNodes) { if (!transportService.nodeConnected(listedNode)) { try { - logger.debug("connecting to listed node (light) [{}]", listedNode); - transportService.connectToNodeLight(listedNode); + logger.debug("connecting to listed node [{}]", listedNode); + transportService.connectToNode(listedNode); } catch (Exception e) { - logger.debug("failed to connect to node [{}], removed from nodes list", e, listedNode); + logger.debug("failed to connect to node [{}]", e); continue; } } @@ -346,7 +349,7 @@ public class TransportClient extends AbstractClient { transportService.connectToNode(node); } catch (Exception e) { it.remove(); - logger.debug("failed to connect to discovered node [" + node + "]", e); + logger.debug("failed to connect to new node [" + node + "], removed", e); } } } @@ -435,20 +438,23 @@ public class TransportClient extends AbstractClient { entries.addAll(searchModule.getNamedWriteables()); entries.addAll(pluginsService.filterPlugins(Plugin.class).stream() .flatMap(p -> p.getNamedWriteables().stream()) - .collect(Collectors.toList())); + .collect(toList())); NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(entries); - + NamedXContentRegistry xContentRegistry = new NamedXContentRegistry(Stream.of( + searchModule.getNamedXContents().stream(), + pluginsService.filterPlugins(Plugin.class).stream() + .flatMap(p -> p.getNamedXContent().stream()) + ).flatMap(Function.identity()).collect(toList())); ModulesBuilder modules = new ModulesBuilder(); // plugin modules must be added here, before others or we can get crazy injection errors for (Module pluginModule : pluginsService.createGuiceModules()) { modules.add(pluginModule); } modules.add(b -> b.bind(ThreadPool.class).toInstance(threadPool)); - ActionModule actionModule = new ActionModule(false, true, - settings, null, settingsModule.getClusterSettings(), + ActionModule actionModule = new ActionModule(true, settings, null, + settingsModule.getClusterSettings(), threadPool, pluginsService.filterPlugins(ActionPlugin.class)); modules.add(actionModule); - CircuitBreakerService circuitBreakerService = Node.createCircuitBreakerService(settingsModule.getSettings(), settingsModule.getClusterSettings()); BigArrays bigArrays = new BigArrays(settings, circuitBreakerService); @@ -457,7 +463,8 @@ public class TransportClient extends AbstractClient { modules.add(settingsModule); NetworkModule networkModule = new NetworkModule(settings, true, pluginsService.filterPlugins(NetworkPlugin.class), threadPool, - bigArrays, circuitBreakerService, namedWriteableRegistry, networkService); + bigArrays, circuitBreakerService, namedWriteableRegistry, + xContentRegistry, networkService); final Transport transport = networkModule.getTransportSupplier().get(); final TransportService transportService = new TransportService(settings, transport, threadPool, networkModule.getTransportInterceptor(), null); @@ -473,10 +480,10 @@ public class TransportClient extends AbstractClient { Injector injector = modules.createInjector(); final ProxyActionMap proxy = new ProxyActionMap(settings, transportService, actionModule.getActions().values().stream() - .map(ActionPlugin.ActionHandler::getAction).collect(Collectors.toList())); + .map(ActionPlugin.ActionHandler::getAction).collect(toList())); List pluginLifecycleComponents = new ArrayList<>(); pluginLifecycleComponents.addAll(pluginsService.getGuiceServiceClasses().stream() - .map(injector::getInstance).collect(Collectors.toList())); + .map(injector::getInstance).collect(toList())); resourcesToClose.addAll(pluginLifecycleComponents); transportService.start(); transportService.acceptIncomingRequests(); From 25f529acd2bf7a4d3ae46ab355ce7f42ed1cf065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sat, 1 Apr 2017 17:17:31 +0200 Subject: [PATCH 08/11] update to Elasticsearch 5.3.0 --- gradle.properties | 6 +- .../java/org/elasticsearch/node/MockNode.java | 1 - .../extras/client/ClusterBlockTest.java | 4 +- .../client/node/BulkNodeClientTest.java | 16 ++-- .../transport/BulkTransportClientTest.java | 31 ++----- .../extras/client/AbstractClient.java | 3 +- .../extras/client/BulkProcessor.java | 89 +++++++++---------- .../extras/client/node/BulkNodeClient.java | 7 +- .../client/transport/BulkTransportClient.java | 41 ++++++--- .../client/transport/MockTransportClient.java | 1 + .../client/transport/TransportClient.java | 31 +++++-- 11 files changed, 121 insertions(+), 109 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2a6c341..95c7fe8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ group = org.xbib name = elasticsearch-extras-client -version = 5.2.2.0 +version = 5.3.0.0 -elasticsearch-client-transport.version = 5.2.2 +elasticsearch-client-transport.version = 5.3.0 xbib-metrics.version = 1.0.0 -netty-transport-native-epoll.version = 4.1.6.Final +netty-transport-native-epoll.version = 4.1.7.Final log4j.version = 2.8 junit.version = 4.12 wagon.version = 2.10 diff --git a/src/integration-test/java/org/elasticsearch/node/MockNode.java b/src/integration-test/java/org/elasticsearch/node/MockNode.java index 10c9e86..7b5d5b3 100644 --- a/src/integration-test/java/org/elasticsearch/node/MockNode.java +++ b/src/integration-test/java/org/elasticsearch/node/MockNode.java @@ -1,7 +1,6 @@ package org.elasticsearch.node; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.plugins.Plugin; import java.util.ArrayList; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java index 07e492f..adfc417 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java @@ -9,6 +9,7 @@ import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; import org.junit.Before; import org.junit.Test; import org.xbib.elasticsearch.NodeTestBase; @@ -45,7 +46,8 @@ public class ClusterBlockTest extends NodeTestBase { BulkRequestBuilder brb = client("1").prepareBulk(); XContentBuilder builder = jsonBuilder().startObject().field("field1", "value1").endObject(); String jsonString = builder.string(); - IndexRequestBuilder irb = client("1").prepareIndex("test", "test", "1").setSource(jsonString); + IndexRequestBuilder irb = client("1").prepareIndex("test", "test", "1") + .setSource(jsonString, XContentType.JSON); brb.add(irb); brb.execute().actionGet(); } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java index 4a295a7..421be5f 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java @@ -142,10 +142,11 @@ public class BulkNodeClientTest extends NodeTestBase { } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); } finally { - assertEquals(numactions, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); } + logger.info("assuring {} == {}", numactions, client.getMetric().getSucceeded().getCount()); + assertEquals(numactions, client.getMetric().getSucceeded().getCount()); assertFalse(client.hasThrowable()); client.shutdown(); } @@ -164,8 +165,7 @@ public class BulkNodeClientTest extends NodeTestBase { .setControl(new SimpleBulkControl()) .toBulkNodeClient(client("1")); try { - client.newIndex("test") - .startBulk("test", 30 * 1000, 1000); + client.newIndex("test").startBulk("test", 30 * 1000, 1000); ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); final CountDownLatch latch = new CountDownLatch(maxthreads); for (int i = 0; i < maxthreads; i++) { @@ -183,11 +183,12 @@ public class BulkNodeClientTest extends NodeTestBase { client.waitForResponses(TimeValue.timeValueSeconds(30)); logger.info("got all responses, executor service shutdown..."); executorService.shutdown(); - logger.info("pool is shut down"); + logger.info("executor service is shut down"); + client.stopBulk("test"); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); } finally { - client.stopBulk("test"); + logger.info("assuring {} == {}", maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); assertEquals(maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); @@ -195,11 +196,12 @@ public class BulkNodeClientTest extends NodeTestBase { assertFalse(client.hasThrowable()); client.refreshIndex("test"); SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) - .setQuery(QueryBuilders.matchAllQuery()).setSize(0); + .setIndices("test") + .setQuery(QueryBuilders.matchAllQuery()) + .setSize(0); assertEquals(maxthreads * maxloop, searchRequestBuilder.execute().actionGet().getHits().getTotalHits()); client.shutdown(); } } - } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java index 205aa88..92ed78c 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java @@ -12,7 +12,6 @@ import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -22,7 +21,6 @@ import org.xbib.elasticsearch.NodeTestBase; import org.xbib.elasticsearch.extras.client.ClientBuilder; import org.xbib.elasticsearch.extras.client.SimpleBulkControl; import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; -import org.xbib.elasticsearch.extras.client.node.BulkNodeClient; import java.io.IOException; import java.util.concurrent.CountDownLatch; @@ -152,7 +150,7 @@ public class BulkTransportClientTest extends NodeTestBase { } @Test - public void testBulkTransportClientRandomDocs() { + public void testBulkTransportClientRandomDocs() throws Exception { long numactions = NUM_ACTIONS; final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) @@ -168,31 +166,25 @@ public class BulkTransportClientTest extends NodeTestBase { } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); - } catch (InterruptedException e) { - // ignore - } catch (ExecutionException e) { - logger.error(e.getMessage(), e); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); - } catch (Throwable t) { - logger.error("unexcepted: " + t.getMessage(), t); } finally { - logger.info("assuring {} == {}", numactions, client.getMetric().getSucceeded().getCount()); - assertEquals(numactions, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); } + logger.info("assuring {} == {}", numactions, client.getMetric().getSucceeded().getCount()); + assertEquals(numactions, client.getMetric().getSucceeded().getCount()); assertFalse(client.hasThrowable()); client.shutdown(); } } @Test - public void testBulkTransportClientThreadedRandomDocs() { + public void testBulkTransportClientThreadedRandomDocs() throws Exception { int maxthreads = Runtime.getRuntime().availableProcessors(); long maxactions = MAX_ACTIONS; final long maxloop = NUM_ACTIONS; - logger.info("firing up client"); + logger.info("TransportClient max={} maxactions={} maxloop={}", maxthreads, maxactions, maxloop); final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, maxactions) @@ -201,23 +193,14 @@ public class BulkTransportClientTest extends NodeTestBase { .setControl(new SimpleBulkControl()) .toBulkTransportClient(); try { - logger.info("new index"); - Settings settingsForIndex = Settings.builder() - .put("index.number_of_shards", 2) - .put("index.number_of_replicas", 1) - .build(); - client.newIndex("test", settingsForIndex, null) - .startBulk("test", -1, 1000); - logger.info("pool"); + client.newIndex("test").startBulk("test", 30 * 1000, 1000); ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); final CountDownLatch latch = new CountDownLatch(maxthreads); for (int i = 0; i < maxthreads; i++) { executorService.execute(() -> { - logger.info("executing runnable"); for (int i1 = 0; i1 < maxloop; i1++) { client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); } - logger.info("done runnable"); latch.countDown(); }); } @@ -232,8 +215,6 @@ public class BulkTransportClientTest extends NodeTestBase { client.stopBulk("test"); } catch (NoNodeAvailableException e) { logger.warn("skipping, no node available"); - } catch (Throwable t) { - logger.error("unexpected error: " + t.getMessage(), t); } finally { logger.info("assuring {} == {}", maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); assertEquals(maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java index ec25b81..20a6e3c 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java @@ -43,6 +43,7 @@ import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; @@ -285,7 +286,7 @@ public abstract class AbstractClient { if (!mappings().isEmpty()) { for (Map.Entry me : mappings().entrySet()) { client().execute(PutMappingAction.INSTANCE, - new PutMappingRequest(index).type(me.getKey()).source(me.getValue())).actionGet(); + new PutMappingRequest(index).type(me.getKey()).source(me.getValue(), XContentType.JSON)).actionGet(); } } } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java index 56a72e5..593d9cc 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java @@ -1,12 +1,12 @@ package org.xbib.elasticsearch.extras.client; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -117,7 +117,7 @@ public class BulkProcessor implements Closeable { if (bulkRequest.numberOfActions() > 0) { execute(); } - return this.bulkRequestHandler.awaitClose(timeout, unit); + return bulkRequestHandler.awaitClose(timeout, unit); } /** @@ -127,8 +127,16 @@ public class BulkProcessor implements Closeable { * @param request request * @return his bulk processor */ - public BulkProcessor add(IndexRequest request) { - return add((ActionRequest) request); + public synchronized BulkProcessor add(IndexRequest request) { + if (request == null) { + return this; + } + ensureOpen(); + bulkRequest.add(request); + if (isOverTheLimit()) { + execute(); + } + return this; } /** @@ -137,64 +145,53 @@ public class BulkProcessor implements Closeable { * @param request request * @return his bulk processor */ - public BulkProcessor add(DeleteRequest request) { - return add((ActionRequest) request); - } - - /** - * Adds either a delete or an index request. - * - * @param request request - * @return his bulk processor - */ - public BulkProcessor add(ActionRequest request) { - return add(request, null); + public synchronized BulkProcessor add(DeleteRequest request) { + if (request == null) { + return this; + } + ensureOpen(); + bulkRequest.add(request); + if (isOverTheLimit()) { + execute(); + } + return this; } /** - * Adds either a delete or an index request with a payload. + * Adds an {@link org.elasticsearch.action.update.UpdateRequest} to the list of actions to execute. * * @param request request - * @param payload payload * @return his bulk processor */ - public BulkProcessor add(ActionRequest request, @Nullable Object payload) { - internalAdd(request, payload); + public synchronized BulkProcessor add(UpdateRequest request) { + if (request == null) { + return this; + } + ensureOpen(); + bulkRequest.add(request); + if (isOverTheLimit()) { + execute(); + } return this; } - protected void ensureOpen() { + private void ensureOpen() { if (closed) { throw new IllegalStateException("bulk process already closed"); } } - private synchronized void internalAdd(ActionRequest request, @Nullable Object payload) { - ensureOpen(); - bulkRequest.add(request, payload); - executeIfNeeded(); - } - - private void executeIfNeeded() { - ensureOpen(); - if (!isOverTheLimit()) { - return; - } - execute(); + private boolean isOverTheLimit() { + final int count = bulkRequest.numberOfActions(); + return count > 0 && + (bulkActions != -1 && count >= bulkActions) || + (bulkSize != -1 && bulkRequest.estimatedSizeInBytes() >= bulkSize); } private void execute() { final BulkRequest myBulkRequest = this.bulkRequest; - final long executionId = executionIdGen.incrementAndGet(); + bulkRequestHandler.execute(myBulkRequest, executionIdGen.incrementAndGet()); this.bulkRequest = new BulkRequest(); - this.bulkRequestHandler.execute(myBulkRequest, executionId); - } - - private boolean isOverTheLimit() { - return bulkActions != -1 && - bulkRequest.numberOfActions() >= bulkActions || - bulkSize != -1 && - bulkRequest.estimatedSizeInBytes() >= bulkSize; } /** @@ -347,17 +344,13 @@ public class BulkProcessor implements Closeable { if (closed) { return; } - if (bulkRequest.numberOfActions() == 0) { - return; + if (bulkRequest.numberOfActions() > 0) { + execute(); } - execute(); } } } - /** - * Abstracts the low-level details of bulk request handling. - */ interface BulkRequestHandler { void execute(BulkRequest bulkRequest, long executionId); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java index bac6bca..7f8edb0 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java @@ -21,6 +21,7 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeValidationException; @@ -241,7 +242,7 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { if (metric != null) { metric.getCurrentIngest().inc(index, type, id); } - bulkProcessor.add(new IndexRequest(index).type(type).id(id).create(false).source(source)); + bulkProcessor.add(new IndexRequest(index).type(type).id(id).create(false).source(source, XContentType.JSON)); } catch (Exception e) { throwable = e; closed = true; @@ -313,7 +314,7 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { if (metric != null) { metric.getCurrentIngest().inc(index, type, id); } - bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source)); + bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); } catch (Exception e) { throwable = e; closed = true; @@ -446,7 +447,7 @@ public class BulkNodeClient extends AbstractClient implements ClientMethods { String type = entry.getKey(); String mapping = entry.getValue(); logger.info("found mapping for {}", type); - createIndexRequestBuilder.addMapping(type, mapping); + createIndexRequestBuilder.addMapping(type, mapping, XContentType.JSON); } } createIndexRequestBuilder.execute().actionGet(); diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java index 3d159ab..2a8193f 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.transport.Netty4Plugin; import org.xbib.elasticsearch.extras.client.AbstractClient; import org.xbib.elasticsearch.extras.client.BulkControl; @@ -293,7 +294,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods String type = entry.getKey(); String mapping = entry.getValue(); logger.info("found mapping for {}", type); - createIndexRequestBuilder.addMapping(type, mapping); + createIndexRequestBuilder.addMapping(type, mapping, XContentType.JSON); } } createIndexRequestBuilder.execute().actionGet(); @@ -348,8 +349,11 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(index, type, id); - bulkProcessor.add(new IndexRequest().index(index).type(type).id(id).create(false).source(source)); + if (metric != null) { + metric.getCurrentIngest().inc(index, type, id); + } + bulkProcessor.add(new IndexRequest().index(index).type(type).id(id).create(false) + .source(source, XContentType.JSON)); } catch (Exception e) { throwable = e; closed = true; @@ -364,7 +368,9 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(indexRequest.index(), indexRequest.type(), indexRequest.id()); + if (metric != null) { + metric.getCurrentIngest().inc(indexRequest.index(), indexRequest.type(), indexRequest.id()); + } bulkProcessor.add(indexRequest); } catch (Exception e) { throwable = e; @@ -380,7 +386,9 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(index, type, id); + if (metric != null) { + metric.getCurrentIngest().inc(index, type, id); + } bulkProcessor.add(new DeleteRequest().index(index).type(type).id(id)); } catch (Exception e) { throwable = e; @@ -396,7 +404,9 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(deleteRequest.index(), deleteRequest.type(), deleteRequest.id()); + if (metric != null) { + metric.getCurrentIngest().inc(deleteRequest.index(), deleteRequest.type(), deleteRequest.id()); + } bulkProcessor.add(deleteRequest); } catch (Exception e) { throwable = e; @@ -412,8 +422,10 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(index, type, id); - bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source)); + if (metric != null) { + metric.getCurrentIngest().inc(index, type, id); + } + bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); } catch (Exception e) { throwable = e; closed = true; @@ -428,7 +440,9 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods throwClose(); } try { - metric.getCurrentIngest().inc(updateRequest.index(), updateRequest.type(), updateRequest.id()); + if (metric != null) { + metric.getCurrentIngest().inc(updateRequest.index(), updateRequest.type(), updateRequest.id()); + } bulkProcessor.add(updateRequest); } catch (Exception e) { throwable = e; @@ -439,7 +453,7 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods } @Override - public synchronized BulkTransportClient flushIngest() { + public BulkTransportClient flushIngest() { if (closed) { throwClose(); } @@ -449,12 +463,13 @@ public class BulkTransportClient extends AbstractClient implements ClientMethods } @Override - public synchronized BulkTransportClient waitForResponses(TimeValue maxWaitTime) - throws InterruptedException, ExecutionException { + public BulkTransportClient waitForResponses(TimeValue maxWaitTime) throws InterruptedException, ExecutionException { if (closed) { throwClose(); } - bulkProcessor.awaitClose(maxWaitTime.getMillis(), TimeUnit.MILLISECONDS); + if (!bulkProcessor.awaitClose(maxWaitTime.getMillis(), TimeUnit.MILLISECONDS)) { + logger.warn("still waiting for responses"); + } return this; } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java index 86199d2..a25d012 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java @@ -1,5 +1,6 @@ package org.xbib.elasticsearch.extras.client.transport; +import org.elasticsearch.action.bulk.BulkProcessor; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.update.UpdateRequest; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java index c639715..6707982 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java @@ -1,7 +1,7 @@ package org.xbib.elasticsearch.extras.client.transport; -import static java.util.stream.Collectors.toList; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; +import static java.util.stream.Collectors.toList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +23,7 @@ import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Injector; @@ -39,8 +40,8 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.node.InternalSettingsPreparer; import org.elasticsearch.node.Node; -import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.NetworkPlugin; import org.elasticsearch.plugins.Plugin; @@ -56,6 +57,7 @@ import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; import java.io.Closeable; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -78,6 +80,8 @@ import java.util.stream.Stream; */ public class TransportClient extends AbstractClient { + private static final Logger logger = LogManager.getLogger(TransportClient.class); + private static final String CLIENT_TYPE = "transport"; private final Injector injector; @@ -452,8 +456,11 @@ public class TransportClient extends AbstractClient { } modules.add(b -> b.bind(ThreadPool.class).toInstance(threadPool)); ActionModule actionModule = new ActionModule(true, settings, null, - settingsModule.getClusterSettings(), threadPool, - pluginsService.filterPlugins(ActionPlugin.class)); + settingsModule.getIndexScopedSettings(), + settingsModule.getClusterSettings(), + settingsModule.getSettingsFilter(), + threadPool, + pluginsService.filterPlugins(ActionPlugin.class), null, null); modules.add(actionModule); CircuitBreakerService circuitBreakerService = Node.createCircuitBreakerService(settingsModule.getSettings(), settingsModule.getClusterSettings()); @@ -464,10 +471,12 @@ public class TransportClient extends AbstractClient { NetworkModule networkModule = new NetworkModule(settings, true, pluginsService.filterPlugins(NetworkPlugin.class), threadPool, bigArrays, circuitBreakerService, namedWriteableRegistry, - xContentRegistry, networkService); + xContentRegistry, networkService, null); final Transport transport = networkModule.getTransportSupplier().get(); final TransportService transportService = new TransportService(settings, transport, threadPool, - networkModule.getTransportInterceptor(), null); + networkModule.getTransportInterceptor(), boundTransportAddress -> + DiscoveryNode.createLocal(settings, dummyAddress(networkModule), UUIDs.randomBase64UUID()), + null); modules.add((b -> { b.bind(BigArrays.class).toInstance(bigArrays); b.bind(PluginsService.class).toInstance(pluginsService); @@ -495,7 +504,15 @@ public class TransportClient extends AbstractClient { } } - private static final Logger logger = LogManager.getLogger(TransportClient.class); + private static TransportAddress dummyAddress(NetworkModule networkModule) { + final TransportAddress address; + try { + address = networkModule.getTransportSupplier().get().addressesFromString("0.0.0.0:0", 1)[0]; + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + return address; + } private static PluginsService newPluginService(final Settings settings, Collection> plugins) { final Settings.Builder settingsBuilder = Settings.builder() From b2ad4f2b255d767a649887d737521097ef728d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sun, 2 Apr 2017 22:06:58 +0200 Subject: [PATCH 09/11] fix duplicate doc tests, ensure no bulk rejection --- gradle.properties | 2 +- .../client/node/BulkNodeDuplicateIDTest.java | 14 ++++++++++---- .../transport/BulkTransportDuplicateIDTest.java | 13 +++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/gradle.properties b/gradle.properties index 95c7fe8..a739c74 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ group = org.xbib name = elasticsearch-extras-client -version = 5.3.0.0 +version = 5.3.0.1 elasticsearch-client-transport.version = 5.3.0 xbib-metrics.version = 1.0.0 diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java index cb32cac..95f3ca8 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java @@ -24,14 +24,15 @@ public class BulkNodeDuplicateIDTest extends NodeTestBase { private static final Logger logger = LogManager.getLogger(BulkNodeDuplicateIDTest.class.getName()); - private static final Long MAX_ACTIONS = 1000L; + private static final long MAX_ACTIONS = 100L; - private static final Long NUM_ACTIONS = 12345L; + private static final long NUM_ACTIONS = 12345L; @Test public void testDuplicateDocIDs() throws Exception { - long numactions = NUM_ACTIONS; + final BulkNodeClient client = ClientBuilder.builder() + .put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) @@ -55,11 +56,16 @@ public class BulkNodeDuplicateIDTest extends NodeTestBase { logger.warn("skipping, no node available"); } finally { client.shutdown(); - assertEquals(numactions, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); } assertFalse(client.hasThrowable()); + logger.info("numactions = {}, submitted = {}, succeeded= {}, failed = {}", NUM_ACTIONS, + client.getMetric().getSubmitted().getCount(), + client.getMetric().getSucceeded().getCount(), + client.getMetric().getFailed().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSubmitted().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSucceeded().getCount()); } } } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java index 148215a..9783dc2 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java +++ b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java @@ -24,15 +24,15 @@ public class BulkTransportDuplicateIDTest extends NodeTestBase { private static final Logger logger = LogManager.getLogger(BulkTransportDuplicateIDTest.class.getName()); - private static final Long MAX_ACTIONS = 1000L; + private static final long MAX_ACTIONS = 100L; - private static final Long NUM_ACTIONS = 12345L; + private static final long NUM_ACTIONS = 12345L; @Test public void testDuplicateDocIDs() throws Exception { - long numactions = NUM_ACTIONS; final BulkTransportClient client = ClientBuilder.builder() .put(getClientSettings()) + .put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) @@ -56,11 +56,16 @@ public class BulkTransportDuplicateIDTest extends NodeTestBase { logger.warn("skipping, no node available"); } finally { client.shutdown(); - assertEquals(numactions, client.getMetric().getSucceeded().getCount()); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); } assertFalse(client.hasThrowable()); + logger.info("numactions = {}, submitted = {}, succeeded= {}, failed = {}", NUM_ACTIONS, + client.getMetric().getSubmitted().getCount(), + client.getMetric().getSucceeded().getCount(), + client.getMetric().getFailed().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSubmitted().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSucceeded().getCount()); } } } From db95d266d3165cfaf612de4182ac9b38a53e79dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Fri, 4 May 2018 16:02:16 +0200 Subject: [PATCH 10/11] working on node/transport/http clients for ES 6.2 --- .gitignore | 5 +- .travis.yml | 5 - README.adoc | 2 +- api/build.gradle | 18 + api/config/checkstyle/checkstyle.xml | 321 +++ api/src/docs/asciidoc/css/foundation.css | 684 +++++++ api/src/docs/asciidoclet/overview.adoc | 4 + backup/XbibTransportService.java | 1047 ++++++++++ build.gradle | 190 +- common/build.gradle | 63 + common/config/checkstyle/checkstyle.xml | 321 +++ common/src/docs/asciidoc/css/foundation.css | 684 +++++++ common/src/docs/asciidoclet/overview.adoc | 4 + .../elasticsearch}/client/AbstractClient.java | 510 ++++- .../elasticsearch}/client/BulkControl.java | 3 +- .../elasticsearch}/client/BulkMetric.java | 3 +- .../elasticsearch}/client/BulkProcessor.java | 84 +- .../elasticsearch}/client/ClientBuilder.java | 53 +- .../elasticsearch}/client/ClientMethods.java | 117 +- .../client/IndexAliasAdder.java | 2 +- .../elasticsearch}/client/NetworkUtils.java | 5 +- .../elasticsearch}/client/Parameters.java | 3 +- .../client/SimpleBulkControl.java | 2 +- .../client/SimpleBulkMetric.java | 40 +- .../elasticsearch/client/package-info.java | 4 + .../client/common/AliasTests.java | 39 +- .../client/common}/NetworkTest.java | 13 +- .../client/common/SearchTests.java | 36 +- .../client/common/SimpleTests.java | 40 +- .../client/common/WildcardTests.java | 51 + .../client/common}/package-info.java | 2 +- .../src/test}/resources/log4j2.xml | 0 gradle.properties | 36 +- gradle/ext.gradle | 8 + gradle/publish.gradle | 2 +- gradle/sonarqube.gradle | 15 +- gradle/wrapper/gradle-wrapper.jar | Bin 54227 -> 54413 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 +- http/build.gradle | 63 + http/config/checkstyle/checkstyle.xml | 323 +++ http/src/docs/asciidoc/css/foundation.css | 684 +++++++ http/src/docs/asciidoclet/overview.adoc | 4 + .../node/info/HttpNodesInfoAction.java | 164 ++ .../HttpClusterUpdateSettingsAction.java | 48 + .../indices/create/HttpCreateIndexAction.java | 35 + .../refresh/HttpRefreshIndexAction.java | 30 + .../put/HttpUpdateSettingsAction.java | 43 + .../action/bulk/HttpBulkAction.java | 69 + .../action/get/HttpExistsAction.java | 29 + .../action/get/HttpGetAction.java | 29 + .../action/index/HttpIndexAction.java | 30 + .../action/main/HttpMainAction.java | 29 + .../action/search/HttpSearchAction.java | 33 + .../action/update/HttpUpdateAction.java | 61 + .../elasticsearch/client/http/HttpAction.java | 158 ++ .../client/http/HttpActionContext.java | 60 + .../client/http/HttpActionFuture.java | 100 + .../elasticsearch/client/http/HttpClient.java | 209 ++ .../client/http/package-info.java | 4 + ...rg.xbib.elasticsearch.client.ClientMethods | 1 + ....xbib.elasticsearch.client.http.HttpAction | 11 + http/src/main/resources/extra-security.policy | 20 + .../client/http/HttpClientAliasTests.java | 109 + .../http/HttpClientDuplicateIDTests.java | 101 + .../client/http/HttpClientReplicaTests.java | 142 ++ .../client/http/HttpClientTests.java | 204 ++ .../HttpClientUpdateReplicaLevelTests.java | 97 + .../client/http/IndexCreationTest.java | 50 + .../client/http/TestRunnerThreadsFilter.java | 11 + .../client/http}/package-info.java | 2 +- node/build.gradle | 63 + node/config/checkstyle/checkstyle.xml | 323 +++ node/src/docs/asciidoc/css/foundation.css | 684 +++++++ node/src/docs/asciidoclet/overview.adoc | 4 + .../client/node/NodeBulkClient.java | 79 + .../client/node/package-info.java | 2 +- ...rg.xbib.elasticsearch.client.ClientMethods | 1 + .../node/NodeBulkClientDuplicateIDTests.java | 37 +- .../node/NodeBulkClientIndexAliasTests.java | 33 +- .../node/NodeBulkClientReplicaTests.java | 48 +- .../client/node/NodeBulkClientTests.java | 99 +- ...NodeBulkClientUpdateReplicaLevelTests.java | 40 +- .../client/node/TestRunnerThreadsFilter.java | 11 + .../client/node/package-info.java | 4 + node/src/test/resources/log4j2.xml | 13 + .../elasticsearch/client/node}/settings.json | 0 settings.gradle | 7 +- .../java/org/elasticsearch/node/MockNode.java | 36 - .../org/elasticsearch/node/package-info.java | 4 - .../org/xbib/elasticsearch/NodeTestBase.java | 233 --- .../extras/client/ClusterBlockTest.java | 55 - .../extras/client/WildcardTest.java | 59 - .../BulkTransportDuplicateIDTest.java | 71 - .../BulkTransportUpdateReplicaLevelTest.java | 68 - .../extras/client/transport/package-info.java | 4 - .../org/xbib/elasticsearch/package-info.java | 4 - .../java/suites/BulkNodeTestSuite.java | 23 - .../java/suites/BulkTransportTestSuite.java | 22 - .../java/suites/ListenerSuite.java | 26 - .../java/suites/MiscTestSuite.java | 21 - .../java/suites/TestListener.java | 44 - .../java/suites/package-info.java | 4 - .../extras/client/node/BulkNodeClient.java | 519 ----- .../extras/client/package-info.java | 4 - .../client/transport/BulkTransportClient.java | 588 ------ .../extras/client/transport/package-info.java | 4 - transport/build.gradle | 63 + transport/config/checkstyle/checkstyle.xml | 323 +++ .../src/docs/asciidoc/css/foundation.css | 684 +++++++ transport/src/docs/asciidoclet/overview.adoc | 4 + .../transport/ByteBufBytesReference.java | 74 + .../client/transport/ByteBufStreamInput.java | 131 ++ .../CompressibleBytesOutputStream.java | 87 + .../client/transport/ConnectionProfile.java | 209 ++ .../client/transport/ESLoggingHandler.java | 108 + .../transport/MockTransportBulkClient.java | 51 +- .../transport/Netty4InternalESLogger.java | 168 ++ .../Netty4MessageChannelHandler.java | 58 + .../client/transport/Netty4Plugin.java | 77 + .../Netty4SizeHeaderFrameDecoder.java | 30 + .../client/transport/Netty4Transport.java | 339 ++++ .../client/transport/Netty4Utils.java | 164 ++ .../client/transport/NettyTcpChannel.java | 92 + .../client/transport/NetworkModule.java | 237 +++ .../client/transport/NetworkPlugin.java | 61 + .../transport/RemoteClusterConnection.java | 728 +++++++ .../transport/RemoteClusterService.java | 385 ++++ .../transport/RemoteConnectionInfo.java | 112 + .../client/transport/TcpTransport.java | 1808 +++++++++++++++++ .../client/transport/TcpTransportChannel.java | 90 + .../client/transport/Transport.java | 116 ++ .../client/transport/TransportBulkClient.java | 129 ++ .../client/transport/TransportClient.java | 210 +- .../TransportConnectionListener.java | 27 + .../transport/TransportInterceptor.java | 31 + .../client/transport/TransportService.java | 1224 +++++++++++ .../client/transport/TransportStatus.java | 52 + .../client/transport/package-info.java | 4 + ...rg.xbib.elasticsearch.client.ClientMethods | 1 + .../src/main/resources/extra-security.policy | 15 + .../transport/TestRunnerThreadsFilter.java | 11 + .../TransportBulkClientDuplicateIDTests.java | 107 + .../TransportBulkClientReplicaTests.java | 70 +- .../transport/TransportBulkClientTests.java | 134 +- ...portBulkClientUpdateReplicaLevelTests.java | 82 + .../client/transport/package-info.java | 4 + 147 files changed, 16332 insertions(+), 2584 deletions(-) create mode 100644 api/build.gradle create mode 100644 api/config/checkstyle/checkstyle.xml create mode 100644 api/src/docs/asciidoc/css/foundation.css create mode 100644 api/src/docs/asciidoclet/overview.adoc create mode 100644 backup/XbibTransportService.java create mode 100644 common/build.gradle create mode 100644 common/config/checkstyle/checkstyle.xml create mode 100644 common/src/docs/asciidoc/css/foundation.css create mode 100644 common/src/docs/asciidoclet/overview.adoc rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/AbstractClient.java (54%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/BulkControl.java (89%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/BulkMetric.java (89%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/BulkProcessor.java (84%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/ClientBuilder.java (55%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/ClientMethods.java (83%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/IndexAliasAdder.java (84%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/NetworkUtils.java (98%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/Parameters.java (94%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/SimpleBulkControl.java (96%) rename {src/main/java/org/xbib/elasticsearch/extras => common/src/main/java/org/xbib/elasticsearch}/client/SimpleBulkMetric.java (53%) create mode 100644 common/src/main/java/org/xbib/elasticsearch/client/package-info.java rename src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java => common/src/test/java/org/xbib/elasticsearch/client/common/AliasTests.java (73%) rename {src/integration-test/java/org/xbib/elasticsearch/extras/client => common/src/test/java/org/xbib/elasticsearch/client/common}/NetworkTest.java (81%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java => common/src/test/java/org/xbib/elasticsearch/client/common/SearchTests.java (73%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java => common/src/test/java/org/xbib/elasticsearch/client/common/SimpleTests.java (67%) create mode 100644 common/src/test/java/org/xbib/elasticsearch/client/common/WildcardTests.java rename {src/integration-test/java/org/xbib/elasticsearch/extras/client => common/src/test/java/org/xbib/elasticsearch/client/common}/package-info.java (52%) rename {src/integration-test => common/src/test}/resources/log4j2.xml (100%) create mode 100644 gradle/ext.gradle create mode 100644 http/build.gradle create mode 100644 http/config/checkstyle/checkstyle.xml create mode 100644 http/src/docs/asciidoc/css/foundation.css create mode 100644 http/src/docs/asciidoclet/overview.adoc create mode 100644 http/src/main/java/org/elasticsearch/action/admin/cluster/node/info/HttpNodesInfoAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/admin/indices/create/HttpCreateIndexAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/admin/indices/refresh/HttpRefreshIndexAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/admin/indices/settings/put/HttpUpdateSettingsAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/bulk/HttpBulkAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/get/HttpExistsAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/get/HttpGetAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/index/HttpIndexAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/main/HttpMainAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/search/HttpSearchAction.java create mode 100644 http/src/main/java/org/elasticsearch/action/update/HttpUpdateAction.java create mode 100644 http/src/main/java/org/xbib/elasticsearch/client/http/HttpAction.java create mode 100644 http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionContext.java create mode 100644 http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionFuture.java create mode 100644 http/src/main/java/org/xbib/elasticsearch/client/http/HttpClient.java create mode 100644 http/src/main/java/org/xbib/elasticsearch/client/http/package-info.java create mode 100644 http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods create mode 100644 http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.http.HttpAction create mode 100644 http/src/main/resources/extra-security.policy create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientAliasTests.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientDuplicateIDTests.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientReplicaTests.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientTests.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientUpdateReplicaLevelTests.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/IndexCreationTest.java create mode 100644 http/src/test/java/org/xbib/elasticsearch/client/http/TestRunnerThreadsFilter.java rename {src/integration-test/java/org/xbib/elasticsearch/extras/client/node => http/src/test/java/org/xbib/elasticsearch/client/http}/package-info.java (56%) create mode 100644 node/build.gradle create mode 100644 node/config/checkstyle/checkstyle.xml create mode 100644 node/src/docs/asciidoc/css/foundation.css create mode 100644 node/src/docs/asciidoclet/overview.adoc create mode 100644 node/src/main/java/org/xbib/elasticsearch/client/node/NodeBulkClient.java rename {src/main/java/org/xbib/elasticsearch/extras => node/src/main/java/org/xbib/elasticsearch}/client/node/package-info.java (52%) create mode 100644 node/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods rename src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java => node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientDuplicateIDTests.java (69%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java => node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientIndexAliasTests.java (67%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java => node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientReplicaTests.java (75%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java => node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientTests.java (70%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java => node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientUpdateReplicaLevelTests.java (62%) create mode 100644 node/src/test/java/org/xbib/elasticsearch/client/node/TestRunnerThreadsFilter.java create mode 100644 node/src/test/java/org/xbib/elasticsearch/client/node/package-info.java create mode 100644 node/src/test/resources/log4j2.xml rename {src/integration-test/resources/org/xbib/elasticsearch/extras/client => node/src/test/resources/org/xbib/elasticsearch/client/node}/settings.json (100%) delete mode 100644 src/integration-test/java/org/elasticsearch/node/MockNode.java delete mode 100644 src/integration-test/java/org/elasticsearch/node/package-info.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java delete mode 100644 src/integration-test/java/org/xbib/elasticsearch/package-info.java delete mode 100644 src/integration-test/java/suites/BulkNodeTestSuite.java delete mode 100644 src/integration-test/java/suites/BulkTransportTestSuite.java delete mode 100644 src/integration-test/java/suites/ListenerSuite.java delete mode 100644 src/integration-test/java/suites/MiscTestSuite.java delete mode 100644 src/integration-test/java/suites/TestListener.java delete mode 100644 src/integration-test/java/suites/package-info.java delete mode 100644 src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java delete mode 100644 src/main/java/org/xbib/elasticsearch/extras/client/package-info.java delete mode 100644 src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java delete mode 100644 src/main/java/org/xbib/elasticsearch/extras/client/transport/package-info.java create mode 100644 transport/build.gradle create mode 100644 transport/config/checkstyle/checkstyle.xml create mode 100644 transport/src/docs/asciidoc/css/foundation.css create mode 100644 transport/src/docs/asciidoclet/overview.adoc create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufBytesReference.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufStreamInput.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/CompressibleBytesOutputStream.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/ConnectionProfile.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/ESLoggingHandler.java rename src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java => transport/src/main/java/org/xbib/elasticsearch/client/transport/MockTransportBulkClient.java (52%) create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4InternalESLogger.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4MessageChannelHandler.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Plugin.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4SizeHeaderFrameDecoder.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Transport.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Utils.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/NettyTcpChannel.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkModule.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkPlugin.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterConnection.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterService.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteConnectionInfo.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransport.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransportChannel.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/Transport.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportBulkClient.java rename {src/main/java/org/xbib/elasticsearch/extras => transport/src/main/java/org/xbib/elasticsearch}/client/transport/TransportClient.java (75%) create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportConnectionListener.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportInterceptor.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportService.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportStatus.java create mode 100644 transport/src/main/java/org/xbib/elasticsearch/client/transport/package-info.java create mode 100644 transport/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods create mode 100644 transport/src/main/resources/extra-security.policy create mode 100644 transport/src/test/java/org/xbib/elasticsearch/client/transport/TestRunnerThreadsFilter.java create mode 100644 transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientDuplicateIDTests.java rename src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java => transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientReplicaTests.java (63%) rename src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java => transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientTests.java (65%) create mode 100644 transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientUpdateReplicaLevelTests.java create mode 100644 transport/src/test/java/org/xbib/elasticsearch/client/transport/package-info.java diff --git a/.gitignore b/.gitignore index bf3e9b4..b92da43 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,10 @@ /target .DS_Store *.iml +*~ /.settings /.classpath /.project /.gradle -/build -/plugins \ No newline at end of file +build +plugins diff --git a/.travis.yml b/.travis.yml index a830350..ee1dfd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,3 @@ jdk: cache: directories: - $HOME/.m2 -after_success: - - ./gradlew sonarqube -Dsonar.host.url=https://sonarqube.com -Dsonar.login=$SONAR_TOKEN -env: - global: - secure: n1Ai4q/yMLn/Pg5pA4lTavoJoe7mQYB1PSKnZAqwbgyla94ySzK6iyBCBiNs/foMPisB/x+DHvmUXTsjvquw9Ay48ZITCV3xhcWzD0eZM2TMoG19CpRAEe8L8LNuYiti9k89ijDdUGZ5ifsvQNTGNHksouayAuApC3PrTUejJfR6SYrp1ZsQTbsMlr+4XU3p7QknK5rGgOwATIMP28F+bVnB05WJtlJA3b0SeucCurn3wJ4FGBQXRYmdlT7bQhNE4QgZM1VzcUFD/K0TBxzzq/otb/lNRSifyoekktDmJwQnaT9uQ4R8R6KdQ2Kb38Rvgjur+TKm5i1G8qS2+6LnIxQJG1aw3JvKK6W0wWCgnAVVRrXaCLday9NuY59tuh1mfjQ10UcsMNKcTdcKEMrLow506wSETcXc7L/LEnneWQyJJeV4vhPqR7KJfsBbeqgz3yIfsCn1GZVWFlfegzYCN52YTl0Y0uRD2Z+TnzQu+Bf4DzaWXLge1rz31xkhyeNNspub4h024+XqBjcMm6M9mlMzmmK8t2DIwPy/BlQbFBUyhrxziuR/5/2NEDPyHltvWkRb4AUIa25WJqkV0gTBegbMadZ9DyOo6Ea7aoVFBae2WGR08F1kzABsWrd1S7UJmWxW35iyMEtoAIayXphIK98qO5aCutwZ+3iOQazxbAs= diff --git a/README.adoc b/README.adoc index aa64451..c5cdd92 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -# Elasticsearch Extras - Client +# Elasticsearch Clients image:https://api.travis-ci.org/xbib/content.svg[title="Build status", link="https://travis-ci.org/jprante/elasticsearch-extras-client/"] image:https://img.shields.io/sonar/http/nemo.sonarqube.com/org.xbib%3Aelasticsearch-extras-client/coverage.svg?style=flat-square[title="Coverage", link="https://sonarqube.com/dashboard/index?id=org.xbib%3Aelasticsearch-extras-client"] diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..02e43a4 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,18 @@ + +dependencies { + compile("org.elasticsearch.client:transport:${rootProject.property('elasticsearch.version')}") { + exclude group: 'org.elasticsearch', module: 'securesm' + exclude group: 'org.elasticsearch.plugin', module: 'transport-netty3-client' + exclude group: 'org.elasticsearch.plugin', module: 'reindex-client' + exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' + exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' + } + // we try to override the Elasticsearch netty by our netty version which might be more recent + compile "io.netty:netty-buffer:${rootProject.property('netty.version')}" + compile "io.netty:netty-codec-http:${rootProject.property('netty.version')}" + compile "io.netty:netty-handler:${rootProject.property('netty.version')}" +} + +jar { + baseName "${rootProject.name}-api" +} diff --git a/api/config/checkstyle/checkstyle.xml b/api/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..8cb4438 --- /dev/null +++ b/api/config/checkstyle/checkstyle.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/src/docs/asciidoc/css/foundation.css b/api/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/api/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/api/src/docs/asciidoclet/overview.adoc b/api/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..7947331 --- /dev/null +++ b/api/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Elasticsearch Java client +Jörg Prante +Version 5.4.0.0 + diff --git a/backup/XbibTransportService.java b/backup/XbibTransportService.java new file mode 100644 index 0000000..c2dc502 --- /dev/null +++ b/backup/XbibTransportService.java @@ -0,0 +1,1047 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.metrics.MeanMetric; +import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.ConcurrentMapLong; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; +import org.elasticsearch.common.util.concurrent.FutureUtils; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.TaskManager; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.ActionNotFoundTransportException; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.FutureTransportResponseHandler; +import org.elasticsearch.transport.NodeDisconnectedException; +import org.elasticsearch.transport.NodeNotConnectedException; +import org.elasticsearch.transport.PlainTransportFuture; +import org.elasticsearch.transport.ReceiveTimeoutTransportException; +import org.elasticsearch.transport.RemoteTransportException; +import org.elasticsearch.transport.RequestHandlerRegistry; +import org.elasticsearch.transport.ResponseHandlerFailureTransportException; +import org.elasticsearch.transport.SendRequestTransportException; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportFuture; +import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportResponseOptions; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * + */ +public class XbibTransportService extends AbstractLifecycleComponent { + + private static final String HANDSHAKE_ACTION_NAME = "internal:transport/handshake"; + + private static final Setting> TRACE_LOG_INCLUDE_SETTING = + Setting.listSetting("transport.tracer.include", Collections.emptyList(), Function.identity(), + Property.Dynamic, Property.NodeScope); + + private static final Setting> TRACE_LOG_EXCLUDE_SETTING = + Setting.listSetting("transport.tracer.exclude", Arrays.asList("internal:discovery/zen/fd*", + TransportLivenessAction.NAME), Function.identity(), Property.Dynamic, Property.NodeScope); + + private final CountDownLatch blockIncomingRequestsLatch = new CountDownLatch(1); + + private final Transport transport; + + private final ThreadPool threadPool; + + private final ClusterName clusterName; + + private final TaskManager taskManager; + + private final TransportInterceptor.AsyncSender asyncSender; + + private final Function localNodeFactory; + + private volatile Map> requestHandlers = Collections.emptyMap(); + + private final Object requestHandlerMutex = new Object(); + + private final ConcurrentMapLong> clientHandlers = + ConcurrentCollections.newConcurrentMapLongWithAggressiveConcurrency(); + + private final TransportInterceptor interceptor; + + // An LRU (don't really care about concurrency here) that holds the latest timed out requests so if they + // do show up, we can print more descriptive information about them + private final Map timeoutInfoHandlers = + Collections.synchronizedMap(new LinkedHashMap(100, .75F, true) { + private static final long serialVersionUID = 9174428975922394994L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 100; + } + }); + + private final Logger tracerLog; + + private volatile String[] tracerLogInclude; + + private volatile String[] tracerLogExclude; + + private volatile DiscoveryNode localNode = null; + + private final Transport.Connection localNodeConnection = new Transport.Connection() { + @Override + public DiscoveryNode getNode() { + return localNode; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + sendLocalRequest(requestId, action, request, options); + } + + @Override + public void close() throws IOException { + } + }; + + /** + * Build the service. + * + * @param clusterSettings if non null the the {@linkplain XbibTransportService} will register + * with the {@link ClusterSettings} for settings updates for + * {@link #TRACE_LOG_EXCLUDE_SETTING} and {@link #TRACE_LOG_INCLUDE_SETTING}. + */ + XbibTransportService(Settings settings, Transport transport, ThreadPool threadPool, + TransportInterceptor transportInterceptor, + Function localNodeFactory, + @Nullable ClusterSettings clusterSettings) { + super(settings); + this.transport = transport; + this.threadPool = threadPool; + this.localNodeFactory = localNodeFactory; + this.clusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); + setTracerLogInclude(TRACE_LOG_INCLUDE_SETTING.get(settings)); + setTracerLogExclude(TRACE_LOG_EXCLUDE_SETTING.get(settings)); + tracerLog = Loggers.getLogger(logger, ".tracer"); + taskManager = createTaskManager(); + this.interceptor = transportInterceptor; + this.asyncSender = interceptor.interceptSender(this::sendRequestInternal); + if (clusterSettings != null) { + clusterSettings.addSettingsUpdateConsumer(TRACE_LOG_INCLUDE_SETTING, this::setTracerLogInclude); + clusterSettings.addSettingsUpdateConsumer(TRACE_LOG_EXCLUDE_SETTING, this::setTracerLogExclude); + } + } + + private TaskManager createTaskManager() { + return new TaskManager(settings); + } + + private void setTracerLogInclude(List tracerLogInclude) { + this.tracerLogInclude = tracerLogInclude.toArray(Strings.EMPTY_ARRAY); + } + + private void setTracerLogExclude(List tracerLogExclude) { + this.tracerLogExclude = tracerLogExclude.toArray(Strings.EMPTY_ARRAY); + } + + @Override + protected void doStart() { + rxMetric.clear(); + txMetric.clear(); + transport.setTransportService(this); + transport.start(); + if (transport.boundAddress() != null && logger.isInfoEnabled()) { + logger.info("{}", transport.boundAddress()); + for (Map.Entry entry : transport.profileBoundAddresses().entrySet()) { + logger.info("profile [{}]: {}", entry.getKey(), entry.getValue()); + } + } + localNode = localNodeFactory.apply(transport.boundAddress()); + registerRequestHandler(HANDSHAKE_ACTION_NAME, + () -> HandshakeRequest.INSTANCE, + ThreadPool.Names.SAME, + (request, channel) -> channel.sendResponse(new HandshakeResponse(localNode, clusterName, + localNode.getVersion()))); + } + + @Override + protected void doStop() { + try { + transport.stop(); + } finally { + // in case the transport is not connected to our local node (thus cleaned on node disconnect) + // make sure to clean any leftover on going handles + for (Map.Entry> entry : clientHandlers.entrySet()) { + final RequestHolder holderToNotify = clientHandlers.remove(entry.getKey()); + if (holderToNotify != null) { + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + threadPool.generic().execute(new AbstractRunnable() { + @Override + public void onRejection(Exception e) { + // if we get rejected during node shutdown we don't wanna bubble it up + logger.debug((Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on rejection, action: {}", + holderToNotify.action()), + e); + } + @Override + public void onFailure(Exception e) { + logger.warn((Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on exception, action: {}", + holderToNotify.action()), + e); + } + @Override + public void doRun() { + TransportException ex = new TransportException("transport stopped, action: " + + holderToNotify.action()); + holderToNotify.handler().handleException(ex); + } + }); + } + } + } + } + + @Override + protected void doClose() { + transport.close(); + } + + /** + * Start accepting incoming requests. + * when the transport layer starts up it will block any incoming requests until + * this method is called + */ + final void acceptIncomingRequests() { + blockIncomingRequestsLatch.countDown(); + } + + /** + * Returns true iff the given node is already connected. + */ + boolean nodeConnected(DiscoveryNode node) { + return isLocalNode(node) || transport.nodeConnected(node); + } + + /** + * Connect to the specified node. + * + * @param node the node to connect to + */ + void connectToNode(final DiscoveryNode node) { + if (isLocalNode(node)) { + return; + } + transport.connectToNode(node, null, (newConnection, actualProfile) -> + handshake(newConnection, actualProfile.getHandshakeTimeout().millis())); + } + + /** + * Executes a high-level handshake using the given connection + * and returns the discovery node of the node the connection + * was established with. The handshake will fail if the cluster + * name on the target node mismatches the local cluster name. + * + * @param connection the connection to a specific node + * @param handshakeTimeout handshake timeout + * @return the connected node + * @throws ConnectTransportException if the connection failed + * @throws IllegalStateException if the handshake failed + */ + private DiscoveryNode handshake(final Transport.Connection connection, + final long handshakeTimeout) throws ConnectTransportException { + return handshake(connection, handshakeTimeout, clusterName::equals); + } + + /** + * Executes a high-level handshake using the given connection + * and returns the discovery node of the node the connection + * was established with. The handshake will fail if the cluster + * name on the target node doesn't match the local cluster name. + * + * @param connection the connection to a specific node + * @param handshakeTimeout handshake timeout + * @param clusterNamePredicate cluster name validation predicate + * @return the connected node + * @throws ConnectTransportException if the connection failed + * @throws IllegalStateException if the handshake failed + */ + private DiscoveryNode handshake(final Transport.Connection connection, + final long handshakeTimeout, Predicate clusterNamePredicate) + throws ConnectTransportException { + final HandshakeResponse response; + final DiscoveryNode node = connection.getNode(); + try { + PlainTransportFuture futureHandler = new PlainTransportFuture<>( + new FutureTransportResponseHandler() { + @Override + public HandshakeResponse newInstance() { + return new HandshakeResponse(); + } + }); + sendRequest(connection, HANDSHAKE_ACTION_NAME, HandshakeRequest.INSTANCE, + TransportRequestOptions.builder().withTimeout(handshakeTimeout).build(), futureHandler); + response = futureHandler.txGet(); + } catch (Exception e) { + throw new IllegalStateException("handshake failed with " + node, e); + } + if (!clusterNamePredicate.test(response.clusterName)) { + throw new IllegalStateException("handshake failed, mismatched cluster name [" + + response.clusterName + "] - " + node); + } else if (!response.version.isCompatible(localNode.getVersion())) { + throw new IllegalStateException("handshake failed, incompatible version [" + + response.version + "] - " + node); + } + return response.discoveryNode; + } + + void disconnectFromNode(DiscoveryNode node) { + if (isLocalNode(node)) { + return; + } + transport.disconnectFromNode(node); + } + + TransportFuture submitRequest(DiscoveryNode node, String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler) + throws TransportException { + PlainTransportFuture futureHandler = new PlainTransportFuture<>(handler); + try { + Transport.Connection connection = getConnection(node); + sendRequest(connection, action, request, options, futureHandler); + } catch (NodeNotConnectedException ex) { + futureHandler.handleException(ex); + } + return futureHandler; + } + + final void sendRequest(final DiscoveryNode node, final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + try { + Transport.Connection connection = getConnection(node); + sendRequest(connection, action, request, options, handler); + } catch (NodeNotConnectedException ex) { + handler.handleException(ex); + } + } + + private void sendRequest(final Transport.Connection connection, final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + + asyncSender.sendRequest(connection, action, request, options, handler); + } + + /** + * Returns either a real transport connection or a local node connection + * if we are using the local node optimization. + * @throws NodeNotConnectedException if the given node is not connected + */ + private Transport.Connection getConnection(DiscoveryNode node) { + if (isLocalNode(node)) { + return localNodeConnection; + } else { + return transport.getConnection(node); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void sendRequestInternal(final Transport.Connection connection, + final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + if (connection == null) { + throw new IllegalStateException("can't send request to a null connection"); + } + DiscoveryNode node = connection.getNode(); + final long requestId = transport.newRequestId(); + final TimeoutHandler timeoutHandler; + try { + if (options.timeout() == null) { + timeoutHandler = null; + } else { + timeoutHandler = new TimeoutHandler(requestId); + } + Supplier storedContextSupplier = + threadPool.getThreadContext().newRestorableContext(true); + TransportResponseHandler responseHandler = + new ContextRestoreResponseHandler<>(storedContextSupplier, handler); + clientHandlers.put(requestId, + new RequestHolder(responseHandler, connection.getNode(), action, timeoutHandler)); + if (lifecycle.stoppedOrClosed()) { + // if we are not started the exception handling will remove the RequestHolder again + // and calls the handler to notify the caller. It will only notify if the toStop code + // hasn't done the work yet. + throw new TransportException("TransportService is closed stopped can't send request"); + } + if (timeoutHandler != null) { + assert options.timeout() != null; + timeoutHandler.future = threadPool.schedule(options.timeout(), ThreadPool.Names.GENERIC, timeoutHandler); + } + connection.sendRequest(requestId, action, request, options); + } catch (final Exception e) { + // usually happen either because we failed to connect to the node + // or because we failed serializing the message + final RequestHolder holderToNotify = clientHandlers.remove(requestId); + // If holderToNotify == null then handler has already been taken care of. + if (holderToNotify != null) { + holderToNotify.cancelTimeout(); + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + final SendRequestTransportException sendRequestException = + new SendRequestTransportException(node, action, e); + threadPool.executor(ThreadPool.Names.GENERIC).execute(new AbstractRunnable() { + @Override + public void onRejection(Exception e) { + // if we get rejected during node shutdown we don't wanna bubble it up + logger.debug((Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on rejection, action: {}", + holderToNotify.action()), e); + } + @Override + public void onFailure(Exception e) { + logger.warn((Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on exception, action: {}", + holderToNotify.action()), e); + } + @Override + protected void doRun() throws Exception { + holderToNotify.handler().handleException(sendRequestException); + } + }); + } else { + logger.debug("Exception while sending request, handler likely already notified due to timeout", e); + } + } + } + + private void sendLocalRequest(long requestId, final String action, final TransportRequest request, + TransportRequestOptions options) { + final DirectResponseChannel channel = new DirectResponseChannel(logger, localNode, action, requestId, adapter, + threadPool); + try { + adapter.onRequestSent(localNode, requestId, action, request, options); + adapter.onRequestReceived(requestId, action); + final RequestHandlerRegistry reg = adapter.getRequestHandler(action); + if (reg == null) { + throw new ActionNotFoundTransportException("Action [" + action + "] not found"); + } + final String executor = reg.getExecutor(); + if (ThreadPool.Names.SAME.equals(executor)) { + reg.processMessageReceived(request, channel); + } else { + threadPool.executor(executor).execute(new AbstractRunnable() { + @Override + protected void doRun() throws Exception { + reg.processMessageReceived(request, channel); + } + + @Override + public boolean isForceExecution() { + return reg.isForceExecution(); + } + + @Override + public void onFailure(Exception e) { + try { + channel.sendResponse(e); + } catch (Exception inner) { + inner.addSuppressed(e); + logger.warn((Supplier) () -> + new ParameterizedMessage("failed to notify channel of error message for action [{}]", + action), inner); + } + } + }); + } + + } catch (Exception e) { + try { + channel.sendResponse(e); + } catch (Exception inner) { + inner.addSuppressed(e); + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "failed to notify channel of error message for action [{}]", action), inner); + } + } + } + + private boolean shouldTraceAction(String action) { + if (tracerLogInclude.length > 0) { + if (!Regex.simpleMatch(tracerLogInclude, action)) { + return false; + } + } + return tracerLogExclude.length <= 0 || !Regex.simpleMatch(tracerLogExclude, action); + } + + /** + * Registers a new request handler. + * + * @param action the action the request handler is associated with + * @param request the request class that will be used to construct new instances for streaming + * @param executor the executor the request handling will be executed on + * @param handler the handler itself that implements the request handling + */ + private void registerRequestHandler(String action, Supplier request, + String executor, + TransportRequestHandler handler) { + handler = interceptor.interceptHandler(action, executor, false, handler); + RequestHandlerRegistry reg = new RequestHandlerRegistry<>( + action, request, taskManager, handler, executor, false, false); + registerRequestHandler(reg); + } + + @SuppressWarnings("unchecked") + private void registerRequestHandler(RequestHandlerRegistry reg) { + synchronized (requestHandlerMutex) { + if (requestHandlers.containsKey(reg.getAction())) { + throw new IllegalArgumentException("transport handlers for action " + + reg.getAction() + " is already registered"); + } + requestHandlers = MapBuilder.newMapBuilder(requestHandlers).put(reg.getAction(), + (RequestHandlerRegistry) reg).immutableMap(); + } + } + + private boolean isLocalNode(DiscoveryNode discoveryNode) { + return Objects.requireNonNull(discoveryNode, "discovery node must not be null").equals(localNode); + } + + static class HandshakeRequest extends TransportRequest { + + static final HandshakeRequest INSTANCE = new HandshakeRequest(); + + private HandshakeRequest() { + } + + } + + /** + * + */ + public static class HandshakeResponse extends TransportResponse { + + private DiscoveryNode discoveryNode; + + private ClusterName clusterName; + + private Version version; + + /** + * For extern construction. + */ + public HandshakeResponse() { + } + + HandshakeResponse(DiscoveryNode discoveryNode, ClusterName clusterName, Version version) { + this.discoveryNode = discoveryNode; + this.version = version; + this.clusterName = clusterName; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + discoveryNode = in.readOptionalWriteable(DiscoveryNode::new); + clusterName = new ClusterName(in); + version = Version.readVersion(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalWriteable(discoveryNode); + clusterName.writeTo(out); + Version.writeVersion(version, out); + } + } + + private final class Adapter implements TransportServiceAdapter { + + final MeanMetric rxMetric = new MeanMetric(); + + final MeanMetric txMetric = new MeanMetric(); + + @Override + public void addBytesReceived(long size) { + rxMetric.inc(size); + } + + @Override + public void addBytesSent(long size) { + txMetric.inc(size); + } + + @Override + public void onRequestSent(DiscoveryNode node, long requestId, String action, TransportRequest request, + TransportRequestOptions options) { + if (traceEnabled() && shouldTraceAction(action)) { + traceRequestSent(node, requestId, action, options); + } + } + + boolean traceEnabled() { + return tracerLog.isTraceEnabled(); + } + + @Override + public void onResponseSent(long requestId, String action, TransportResponse response, + TransportResponseOptions options) { + if (traceEnabled() && shouldTraceAction(action)) { + traceResponseSent(requestId, action); + } + } + + @Override + public void onResponseSent(long requestId, String action, Exception e) { + if (traceEnabled() && shouldTraceAction(action)) { + traceResponseSent(requestId, action, e); + } + } + + void traceResponseSent(long requestId, String action, Exception e) { + tracerLog.trace( + (org.apache.logging.log4j.util.Supplier) + () -> new ParameterizedMessage("[{}][{}] sent error response", requestId, action), e); + } + + @Override + public void onRequestReceived(long requestId, String action) { + try { + blockIncomingRequestsLatch.await(); + } catch (InterruptedException e) { + logger.trace("interrupted while waiting for incoming requests block to be removed"); + } + if (traceEnabled() && shouldTraceAction(action)) { + traceReceivedRequest(requestId, action); + } + } + + @Override + public RequestHandlerRegistry getRequestHandler(String action) { + return requestHandlers.get(action); + } + + @Override + public TransportResponseHandler onResponseReceived(final long requestId) { + RequestHolder holder = clientHandlers.remove(requestId); + if (holder == null) { + checkForTimeout(requestId); + return null; + } + holder.cancelTimeout(); + if (traceEnabled() && shouldTraceAction(holder.action())) { + traceReceivedResponse(requestId, holder.node(), holder.action()); + } + return holder.handler(); + } + + void checkForTimeout(long requestId) { + // lets see if its in the timeout holder, but sync on mutex to make sure any ongoing timeout + // handling has finished + final DiscoveryNode sourceNode; + final String action; + if (clientHandlers.get(requestId) != null) { + throw new IllegalStateException(); + } + TimeoutInfoHolder timeoutInfoHolder = timeoutInfoHandlers.remove(requestId); + if (timeoutInfoHolder != null) { + long time = System.currentTimeMillis(); + logger.warn("Received response for a request that has timed out, sent [{}ms] ago, timed out [{}ms] ago, " + + "action [{}], node [{}], id [{}]", time - timeoutInfoHolder.sentTime(), + time - timeoutInfoHolder.timeoutTime(), + timeoutInfoHolder.action(), timeoutInfoHolder.node(), requestId); + action = timeoutInfoHolder.action(); + sourceNode = timeoutInfoHolder.node(); + } else { + logger.warn("Transport response handler not found of id [{}]", requestId); + action = null; + sourceNode = null; + } + // call tracer out of lock + if (!traceEnabled()) { + return; + } + if (action == null) { + assert sourceNode == null; + traceUnresolvedResponse(requestId); + } else if (shouldTraceAction(action)) { + traceReceivedResponse(requestId, sourceNode, action); + } + } + + @Override + public void onNodeConnected(final DiscoveryNode node) { + } + + @Override + public void onConnectionOpened(DiscoveryNode node) { + } + + @Override + public void onNodeDisconnected(final DiscoveryNode node) { + try { + for (Map.Entry> entry : clientHandlers.entrySet()) { + RequestHolder holder = entry.getValue(); + if (holder.node().equals(node)) { + final RequestHolder holderToNotify = clientHandlers.remove(entry.getKey()); + if (holderToNotify != null) { + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + threadPool.generic().execute(() -> holderToNotify.handler() + .handleException(new NodeDisconnectedException(node, + holderToNotify.action()))); + } + } + } + } catch (EsRejectedExecutionException ex) { + logger.debug("Rejected execution on NodeDisconnected", ex); + } + } + + void traceReceivedRequest(long requestId, String action) { + tracerLog.trace("[{}][{}] received request", requestId, action); + } + + void traceResponseSent(long requestId, String action) { + tracerLog.trace("[{}][{}] sent response", requestId, action); + } + + void traceReceivedResponse(long requestId, DiscoveryNode sourceNode, String action) { + tracerLog.trace("[{}][{}] received response from [{}]", requestId, action, sourceNode); + } + + void traceUnresolvedResponse(long requestId) { + tracerLog.trace("[{}] received response but can't resolve it to a request", requestId); + } + + void traceRequestSent(DiscoveryNode node, long requestId, String action, TransportRequestOptions options) { + tracerLog.trace("[{}][{}] sent to [{}] (timeout: [{}])", requestId, action, node, options.timeout()); + } + } + + private final class TimeoutHandler implements Runnable { + + private final long requestId; + + private final long sentTime = System.currentTimeMillis(); + + volatile ScheduledFuture future; + + TimeoutHandler(long requestId) { + this.requestId = requestId; + } + + @Override + public void run() { + // we get first to make sure we only add the TimeoutInfoHandler if needed. + final RequestHolder holder = clientHandlers.get(requestId); + if (holder != null) { + // add it to the timeout information holder, in case we are going to get a response later + long timeoutTime = System.currentTimeMillis(); + timeoutInfoHandlers.put(requestId, new TimeoutInfoHolder(holder.node(), holder.action(), sentTime, + timeoutTime)); + // now that we have the information visible via timeoutInfoHandlers, we try to remove the request id + final RequestHolder removedHolder = clientHandlers.remove(requestId); + if (removedHolder != null) { + assert removedHolder == holder : "two different holder instances for request [" + requestId + "]"; + removedHolder.handler().handleException( + new ReceiveTimeoutTransportException(holder.node(), holder.action(), + "request_id [" + requestId + "] timed out after [" + (timeoutTime - sentTime) + "ms]")); + } else { + // response was processed, remove timeout info. + timeoutInfoHandlers.remove(requestId); + } + } + } + + /** + * Cancels timeout handling. This is a best effort only to avoid running it. + * Remove the requestId from {@link #clientHandlers} to make sure this doesn't run. + */ + void cancel() { + if (clientHandlers.get(requestId) != null) { + throw new IllegalStateException("cancel must be called after the requestId [" + + requestId + "] has been removed from clientHandlers"); + } + FutureUtils.cancel(future); + } + } + + private static class TimeoutInfoHolder { + + private final DiscoveryNode node; + private final String action; + private final long sentTime; + private final long timeoutTime; + + TimeoutInfoHolder(DiscoveryNode node, String action, long sentTime, long timeoutTime) { + this.node = node; + this.action = action; + this.sentTime = sentTime; + this.timeoutTime = timeoutTime; + } + + public DiscoveryNode node() { + return node; + } + + String action() { + return action; + } + + long sentTime() { + return sentTime; + } + + long timeoutTime() { + return timeoutTime; + } + } + + private static class RequestHolder { + + private final TransportResponseHandler handler; + + private final DiscoveryNode node; + + private final String action; + + private final TimeoutHandler timeoutHandler; + + RequestHolder(TransportResponseHandler handler, DiscoveryNode node, String action, + TimeoutHandler timeoutHandler) { + this.handler = handler; + this.node = node; + this.action = action; + this.timeoutHandler = timeoutHandler; + } + + TransportResponseHandler handler() { + return handler; + } + + public DiscoveryNode node() { + return this.node; + } + + String action() { + return this.action; + } + + void cancelTimeout() { + if (timeoutHandler != null) { + timeoutHandler.cancel(); + } + } + } + + /** + * This handler wrapper ensures that the response thread executes with the correct thread context. + * Before any of the handle methods are invoked we restore the context. + * @param thr transport response type + */ + public static final class ContextRestoreResponseHandler + implements TransportResponseHandler { + + private final TransportResponseHandler delegate; + + private final Supplier contextSupplier; + + ContextRestoreResponseHandler(Supplier contextSupplier, + TransportResponseHandler delegate) { + this.delegate = delegate; + this.contextSupplier = contextSupplier; + } + + @Override + public T newInstance() { + return delegate.newInstance(); + } + + @SuppressWarnings("try") + @Override + public void handleResponse(T response) { + try (ThreadContext.StoredContext ignore = contextSupplier.get()) { + delegate.handleResponse(response); + } + } + + @SuppressWarnings("try") + @Override + public void handleException(TransportException exp) { + try (ThreadContext.StoredContext ignore = contextSupplier.get()) { + delegate.handleException(exp); + } + } + + @Override + public String executor() { + return delegate.executor(); + } + + @Override + public String toString() { + return getClass().getName() + "/" + delegate.toString(); + } + + } + + static class DirectResponseChannel implements TransportChannel { + + private static final String DIRECT_RESPONSE_PROFILE = ".direct"; + + private final Logger logger; + + private final DiscoveryNode localNode; + + private final String action; + + private final long requestId; + + private final TransportServiceAdapter adapter; + + private final ThreadPool threadPool; + + DirectResponseChannel(Logger logger, DiscoveryNode localNode, String action, long requestId, + TransportServiceAdapter adapter, ThreadPool threadPool) { + this.logger = logger; + this.localNode = localNode; + this.action = action; + this.requestId = requestId; + this.adapter = adapter; + this.threadPool = threadPool; + } + + @Override + public String action() { + return action; + } + + @Override + public String getProfileName() { + return DIRECT_RESPONSE_PROFILE; + } + + @Override + public void sendResponse(TransportResponse response) throws IOException { + sendResponse(response, TransportResponseOptions.EMPTY); + } + + @SuppressWarnings("unchecked") + @Override + public void sendResponse(final TransportResponse response, TransportResponseOptions options) + throws IOException { + adapter.onResponseSent(requestId, action, response, options); + final TransportResponseHandler handler = adapter.onResponseReceived(requestId); + if (handler != null) { + final String executor = handler.executor(); + if (ThreadPool.Names.SAME.equals(executor)) { + processResponse(handler, response); + } else { + threadPool.executor(executor).execute(() -> processResponse(handler, response)); + } + } + } + + void processResponse(TransportResponseHandler handler, TransportResponse response) { + try { + handler.handleResponse(response); + } catch (Exception e) { + processException(handler, wrapInRemote(new ResponseHandlerFailureTransportException(e))); + } + } + + @SuppressWarnings("unchecked") + @Override + public void sendResponse(Exception exception) throws IOException { + adapter.onResponseSent(requestId, action, exception); + final TransportResponseHandler handler = adapter.onResponseReceived(requestId); + if (handler != null) { + final RemoteTransportException rtx = wrapInRemote(exception); + final String executor = handler.executor(); + if (ThreadPool.Names.SAME.equals(executor)) { + processException(handler, rtx); + } else { + threadPool.executor(handler.executor()).execute(() -> processException(handler, rtx)); + } + } + } + + RemoteTransportException wrapInRemote(Exception e) { + if (e instanceof RemoteTransportException) { + return (RemoteTransportException) e; + } + return new RemoteTransportException(localNode.getName(), localNode.getAddress(), action, e); + } + + void processException(final TransportResponseHandler handler, final RemoteTransportException rtx) { + try { + handler.handleException(rtx); + } catch (Exception e) { + logger.error((Supplier) () -> new ParameterizedMessage( + "failed to handle exception for action [{}], handler [{}]", action, handler), e); + } + } + + @Override + public long getRequestId() { + return requestId; + } + + @Override + public String getChannelType() { + return "direct"; + } + + @Override + public Version getVersion() { + return localNode.getVersion(); + } + } +} diff --git a/build.gradle b/build.gradle index 87b3d58..4a6e6c8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ plugins { - id "org.sonarqube" version "2.2" + id "org.sonarqube" version "2.6.1" + id "io.codearte.nexus-staging" version "0.11.0" + id "org.xbib.gradle.plugin.asciidoctor" version "1.6.0.0" } printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + @@ -16,114 +18,112 @@ printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + GroovySystem.getVersion(), gradle.gradleVersion -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'signing' -apply plugin: 'findbugs' -apply plugin: 'pmd' -apply plugin: 'checkstyle' -apply plugin: "jacoco" - -ext { - user = 'xbib' - name = 'elasticsearch-extras-client' - description = 'Some extras implemented for using Elasticsearch clients (node and transport)' - scmUrl = 'https://github.com/' + user + '/' + name - scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' -} - -sourceSets { - integrationTest { - java { - srcDir file('src/integration-test/java') - compileClasspath += main.output - compileClasspath += test.output - } - resources { - srcDir file('src/integration-test/resources') - } +apply plugin: "io.codearte.nexus-staging" + +subprojects { + apply plugin: 'java' + apply plugin: 'maven' + apply plugin: 'signing' + //apply plugin: 'findbugs' + //apply plugin: 'pmd' + //apply plugin: 'checkstyle' + apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + + configurations { + wagon + alpnagent + asciidoclet } -} -repositories { - mavenCentral() -} + dependencies { + //testCompile "junit:junit:${project.property('junit.version')}" + //testCompile "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" + asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}" + wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" + } -configurations { - wagon - integrationTestCompile.extendsFrom testCompile - integrationTestRuntime.extendsFrom testRuntime -} + sourceCompatibility = JavaVersion.VERSION_1_9 + targetCompatibility = JavaVersion.VERSION_1_8 + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + tasks.withType(JavaCompile) { + options.compilerArgs << "-proc:none" << "-Xlint:all,-rawtypes,-unchecked,-try" << "--release" << "8" //<< "-profile" << "compact3" + } -dependencies { - compile("org.elasticsearch.client:transport:${project.property('elasticsearch-client-transport.version')}") { - exclude group: 'org.elasticsearch', module: 'securesm' - exclude group: 'org.elasticsearch.plugin', module: 'transport-netty3-client' - exclude group: 'org.elasticsearch.plugin', module: 'reindex-client' - exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' - exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' + jar { + baseName "${rootProject.name}-${project.name}" } - compile "org.xbib:metrics:${project.property('xbib-metrics.version')}" - compile "io.netty:netty-transport-native-epoll:${project.property('netty-transport-native-epoll.version')}:linux-x86_64" - compile "org.apache.logging.log4j:log4j-api:${project.property('log4j.version')}" - testCompile "junit:junit:${project.property('junit.version')}" - testCompile "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" - wagon "org.apache.maven.wagon:wagon-ssh-external:${project.property('wagon.version')}" -} -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 + asciidoctor { + attributes toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + 'source-highlighter': 'coderay' + } -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" << "-profile" << "compact3" -} + javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = 'org.xbib.asciidoclet.Asciidoclet' + options.overview = "src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}" + configure(options) { + noTimestamp = true + } + } -task integrationTest(type: Test, group: 'verification') { - include '**/MiscTestSuite.class' - include '**/BulkNodeTestSuite.class' - include '**/BulkTransportTestSuite.class' - testClassesDir = sourceSets.integrationTest.output.classesDir - classpath = configurations.integrationTestCompile - classpath += configurations.integrationTestRuntime - classpath += sourceSets.main.output - classpath += sourceSets.test.output - classpath += sourceSets.integrationTest.output - outputs.upToDateWhen { false } - systemProperty 'path.home', projectDir.absolutePath - testLogging.showStandardStreams = false -} + /*task javadocJar(type: Jar, dependsOn: classes) { + baseName "${rootProject.name}-${project.name}" + from javadoc + into "build/tmp" + classifier 'javadoc' + } -integrationTest.mustRunAfter test -check.dependsOn integrationTest + task sourcesJar(type: Jar, dependsOn: classes) { + baseName "${rootProject.name}-${project.name}" + from sourceSets.main.allSource + into "build/tmp" + classifier 'sources' + } -clean { - delete "plugins" - delete "logs" -} + artifacts { + archives javadocJar, sourcesJar + }*/ -task javadocJar(type: Jar, dependsOn: classes) { - from javadoc - into "build/tmp" - classifier 'javadoc' -} + if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } + } -task sourcesJar(type: Jar, dependsOn: classes) { - from sourceSets.main.allSource - into "build/tmp" - classifier 'sources' -} + apply from: "${rootProject.projectDir}/gradle/ext.gradle" + apply from: "${rootProject.projectDir}/gradle/publish.gradle" + //apply from: "${rootProject.projectDir}/gradle/sonarqube.gradle" -artifacts { - archives javadocJar, sourcesJar } -if (project.hasProperty('signing.keyId')) { - signing { - sign configurations.archives +/* +task aggregatedJavadoc(type: Javadoc) { + group = 'aggregation' + description = 'Generates aggregated Javadoc API documentation.' + title = "$description $version API" + destinationDir = file("$buildDir/docs/javadoc") + def sourceProjects = subprojects.findAll { + it.plugins.hasPlugin('java') || it.plugins.hasPlugin('groovy') + } + source sourceProjects.collect { + it.sourceSets.main.allJava } + classpath = files(sourceProjects.collect { + it.sourceSets.main.runtimeClasspath + }) + //options.overview = 'gradle/api/overview.html' + options.showFromProtected() } - -apply from: 'gradle/publish.gradle' -apply from: 'gradle/sonarqube.gradle' +*/ diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..8638f17 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,63 @@ +buildscript { + repositories { + jcenter() + maven { + url 'http://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.2.2.0" + } +} + +apply plugin: 'org.xbib.gradle.plugin.elasticsearch.build' + +configurations { + main + tests +} + +dependencies { + compile project(':api') + compile "org.xbib:metrics:${project.property('xbib-metrics.version')}" + compileOnly "org.apache.logging.log4j:log4j-api:${project.property('log4j.version')}" + testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" + testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" +} + +jar { + baseName "${rootProject.name}-common" +} + +task testJar(type: Jar, dependsOn: testClasses) { + baseName = "${project.archivesBaseName}-tests" + from sourceSets.test.output +} + +artifacts { + main jar + tests testJar + archives sourcesJar, javadocJar +} + +test { + enabled = false + jvmArgs "-javaagent:" + configurations.alpnagent.asPath + systemProperty 'path.home', project.buildDir.absolutePath + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} + +randomizedTest { + enabled = false +} + +esTest { + // test with the jars, not the classes, for security manager + classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files + systemProperty 'tests.security.manager', 'true' +} +esTest.dependsOn jar, testJar + diff --git a/common/config/checkstyle/checkstyle.xml b/common/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..8cb4438 --- /dev/null +++ b/common/config/checkstyle/checkstyle.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/docs/asciidoc/css/foundation.css b/common/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/common/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/common/src/docs/asciidoclet/overview.adoc b/common/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..7947331 --- /dev/null +++ b/common/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Elasticsearch Java client +Jörg Prante +Version 5.4.0.0 + diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java b/common/src/main/java/org/xbib/elasticsearch/client/AbstractClient.java similarity index 54% rename from src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java rename to common/src/main/java/org/xbib/elasticsearch/client/AbstractClient.java index 20a6e3c..79a9336 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/AbstractClient.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/AbstractClient.java @@ -1,9 +1,10 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; @@ -16,6 +17,9 @@ import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder import org.elasticsearch.action.admin.indices.alias.get.GetAliasesAction; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequestBuilder; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexAction; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse; @@ -26,6 +30,7 @@ import org.elasticsearch.action.admin.indices.get.GetIndexRequestBuilder; import org.elasticsearch.action.admin.indices.get.GetIndexResponse; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder; import org.elasticsearch.action.admin.indices.recovery.RecoveryAction; import org.elasticsearch.action.admin.indices.recovery.RecoveryRequest; import org.elasticsearch.action.admin.indices.recovery.RecoveryResponse; @@ -33,15 +38,23 @@ import org.elasticsearch.action.admin.indices.refresh.RefreshAction; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction; import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.search.SearchHit; @@ -53,6 +66,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -61,13 +75,15 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * */ -public abstract class AbstractClient { +public abstract class AbstractClient implements ClientMethods { private static final Logger logger = LogManager.getLogger(AbstractClient.class.getName()); @@ -75,16 +91,168 @@ public abstract class AbstractClient { private Settings settings; - private Map mappings = new HashMap<>(); + private Map mappings; - public abstract ElasticsearchClient client(); + private ElasticsearchClient client; - protected abstract void createClient(Settings settings) throws IOException; + protected BulkProcessor bulkProcessor; - public abstract void shutdown(); + protected BulkMetric metric; - public Settings.Builder getSettingsBuilder() { - return settingsBuilder(); + protected BulkControl control; + + protected Throwable throwable; + + protected boolean closed; + + protected int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; + + protected int maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS; + + protected ByteSizeValue maxVolume = DEFAULT_MAX_VOLUME_PER_REQUEST; + + protected TimeValue flushInterval = DEFAULT_FLUSH_INTERVAL; + + @Override + public AbstractClient init(ElasticsearchClient client, Settings settings, + final BulkMetric metric, final BulkControl control) { + this.client = client; + this.mappings = new HashMap<>(); + if (settings == null) { + settings = findSettings(); + } + if (client == null && settings != null) { + try { + this.client = createClient(settings); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + this.metric = metric; + this.control = control; + if (metric != null) { + metric.start(); + } + resetSettings(); + BulkProcessor.Listener listener = new BulkProcessor.Listener() { + + private final Logger logger = LogManager.getLogger(getClass().getName() + ".Listener"); + + @Override + public void beforeBulk(long executionId, BulkRequest request) { + long l = -1; + if (metric != null) { + metric.getCurrentIngest().inc(); + l = metric.getCurrentIngest().getCount(); + int n = request.numberOfActions(); + metric.getSubmitted().inc(n); + metric.getCurrentIngestNumDocs().inc(n); + metric.getTotalIngestSizeInBytes().inc(request.estimatedSizeInBytes()); + } + logger.debug("before bulk [{}] [actions={}] [bytes={}] [concurrent requests={}]", + executionId, + request.numberOfActions(), + request.estimatedSizeInBytes(), + l); + } + + @Override + public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { + long l = -1; + if (metric != null) { + metric.getCurrentIngest().dec(); + l = metric.getCurrentIngest().getCount(); + metric.getSucceeded().inc(response.getItems().length); + } + int n = 0; + for (BulkItemResponse itemResponse : response.getItems()) { + if (metric != null) { + metric.getCurrentIngest().dec(itemResponse.getIndex(), itemResponse.getType(), itemResponse.getId()); + } + if (itemResponse.isFailed()) { + n++; + if (metric != null) { + metric.getSucceeded().dec(1); + metric.getFailed().inc(1); + } + } + } + if (metric != null) { + logger.debug("after bulk [{}] [succeeded={}] [failed={}] [{}ms] {} concurrent requests", + executionId, + metric.getSucceeded().getCount(), + metric.getFailed().getCount(), + response.getTook().millis(), + l); + } + if (n > 0) { + logger.error("bulk [{}] failed with {} failed items, failure message = {}", + executionId, n, response.buildFailureMessage()); + } else { + if (metric != null) { + metric.getCurrentIngestNumDocs().dec(response.getItems().length); + } + } + } + + @Override + public void afterBulk(long executionId, BulkRequest request, Throwable failure) { + if (metric != null) { + metric.getCurrentIngest().dec(); + } + throwable = failure; + closed = true; + logger.error("after bulk [" + executionId + "] error", failure); + } + }; + if (this.client != null) { + BulkProcessor.Builder builder = BulkProcessor.builder(this.client, listener) + .setBulkActions(maxActionsPerRequest) + .setConcurrentRequests(maxConcurrentRequests) + .setFlushInterval(flushInterval); + if (maxVolume != null) { + builder.setBulkSize(maxVolume); + } + this.bulkProcessor = builder.build(); + } + this.closed = false; + return this; + } + + protected abstract ElasticsearchClient createClient(Settings settings) throws IOException; + + @Override + public ElasticsearchClient client() { + return client; + } + + @Override + public ClientMethods maxActionsPerRequest(int maxActionsPerRequest) { + this.maxActionsPerRequest = maxActionsPerRequest; + return this; + } + + @Override + public ClientMethods maxConcurrentRequests(int maxConcurrentRequests) { + this.maxConcurrentRequests = maxConcurrentRequests; + return this; + } + + @Override + public ClientMethods maxVolumePerRequest(ByteSizeValue maxVolume) { + this.maxVolume = maxVolume; + return this; + } + + @Override + public ClientMethods flushIngestInterval(TimeValue flushInterval) { + this.flushInterval = flushInterval; + return this; + } + + @Override + public BulkMetric getMetric() { + return metric; } public void resetSettings() { @@ -119,7 +287,7 @@ public abstract class AbstractClient { } public void setting(InputStream in) throws IOException { - settingsBuilder = Settings.builder().loadFromStream(".json", in); + settingsBuilder = Settings.builder().loadFromStream(".json", in, true); } public Settings.Builder settingsBuilder() { @@ -136,42 +304,248 @@ public abstract class AbstractClient { return settingsBuilder.build(); } + @Override public void mapping(String type, String mapping) throws IOException { mappings.put(type, mapping); } + @Override public void mapping(String type, InputStream in) throws IOException { if (type == null) { return; } StringWriter sw = new StringWriter(); - Streams.copy(new InputStreamReader(in), sw); + Streams.copy(new InputStreamReader(in, StandardCharsets.UTF_8), sw); mappings.put(type, sw.toString()); } - public Map mappings() { - return mappings.isEmpty() ? null : mappings; + @Override + public ClientMethods index(String index, String type, String id, boolean create, BytesReference source) { + return indexRequest(new IndexRequest(index).type(type).id(id).create(create).source(source, XContentType.JSON)); } + @Override + public ClientMethods index(String index, String type, String id, boolean create, String source) { + return indexRequest(new IndexRequest(index).type(type).id(id).create(create).source(source, XContentType.JSON)); + } - public void updateIndexSetting(String index, String key, Object value) throws IOException { + @Override + public ClientMethods indexRequest(IndexRequest indexRequest) { + if (closed) { + throwClose(); + } + try { + if (metric != null) { + metric.getCurrentIngest().inc(indexRequest.index(), indexRequest.type(), indexRequest.id()); + } + bulkProcessor.add(indexRequest); + } catch (Exception e) { + throwable = e; + closed = true; + logger.error("bulk add of index request failed: " + e.getMessage(), e); + } + return this; + } + + @Override + public ClientMethods delete(String index, String type, String id) { + return deleteRequest(new DeleteRequest(index).type(type).id(id)); + } + + @Override + public ClientMethods deleteRequest(DeleteRequest deleteRequest) { + if (closed) { + throwClose(); + } + try { + if (metric != null) { + metric.getCurrentIngest().inc(deleteRequest.index(), deleteRequest.type(), deleteRequest.id()); + } + bulkProcessor.add(deleteRequest); + } catch (Exception e) { + throwable = e; + closed = true; + logger.error("bulk add of delete failed: " + e.getMessage(), e); + } + return this; + } + + @Override + public ClientMethods update(String index, String type, String id, BytesReference source) { + return updateRequest(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); + } + + @Override + public ClientMethods update(String index, String type, String id, String source) { + return updateRequest(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); + } + + @Override + public ClientMethods updateRequest(UpdateRequest updateRequest) { + if (closed) { + throwClose(); + } + try { + if (metric != null) { + metric.getCurrentIngest().inc(updateRequest.index(), updateRequest.type(), updateRequest.id()); + } + bulkProcessor.add(updateRequest); + } catch (Exception e) { + throwable = e; + closed = true; + logger.error("bulk add of update request failed: " + e.getMessage(), e); + } + return this; + } + + @Override + public ClientMethods startBulk(String index, long startRefreshIntervalSeconds, long stopRefreshIntervalSeconds) + throws IOException { + if (control == null) { + return this; + } + if (!control.isBulk(index) && startRefreshIntervalSeconds > 0L && stopRefreshIntervalSeconds > 0L) { + control.startBulk(index, startRefreshIntervalSeconds, stopRefreshIntervalSeconds); + updateIndexSetting(index, "refresh_interval", startRefreshIntervalSeconds + "s"); + } + return this; + } + + @Override + public ClientMethods stopBulk(String index) throws IOException { + if (control == null) { + return this; + } + if (control.isBulk(index)) { + long secs = control.getStopBulkRefreshIntervals().get(index); + if (secs > 0L) { + updateIndexSetting(index, "refresh_interval", secs + "s"); + } + control.finishBulk(index); + } + return this; + } + + @Override + public ClientMethods flushIngest() { + if (closed) { + throwClose(); + } + logger.debug("flushing bulk processor"); + bulkProcessor.flush(); + return this; + } + + @Override + public synchronized void shutdown() throws IOException { + if (closed) { + throwClose(); + } + if (bulkProcessor != null) { + logger.info("closing bulk processor..."); + bulkProcessor.close(); + } + if (metric != null) { + logger.info("stopping metric"); + metric.stop(); + } + if (control != null && control.indices() != null && !control.indices().isEmpty()) { + logger.info("stopping bulk mode for indices {}...", control.indices()); + for (String index : control.indices()) { + stopBulk(index); + } + } + } + + @Override + public ClientMethods newIndex(String index) { + if (closed) { + throwClose(); + } + return newIndex(index, null, null); + } + + @Override + public ClientMethods newIndex(String index, String type, InputStream settings, InputStream mappings) throws IOException { + resetSettings(); + setting(settings); + mapping(type, mappings); + return newIndex(index, settings(), this.mappings); + } + + @Override + public ClientMethods newIndex(String index, Settings settings, Map mappings) { + if (closed) { + throwClose(); + } if (client() == null) { - return; + logger.warn("no client for create index"); + return this; } if (index == null) { - throw new IOException("no index name given"); + logger.warn("no index name given to create index"); + return this; } - if (key == null) { - throw new IOException("no key given"); + CreateIndexRequestBuilder createIndexRequestBuilder = + new CreateIndexRequestBuilder(client(), CreateIndexAction.INSTANCE).setIndex(index); + if (settings != null) { + logger.info("found settings {}", settings.toString()); + createIndexRequestBuilder.setSettings(settings); + } + if (mappings != null) { + for (Map.Entry entry : mappings.entrySet()) { + String type = entry.getKey(); + String mapping = entry.getValue(); + logger.info("found mapping for {}", type); + createIndexRequestBuilder.addMapping(type, mapping, XContentType.JSON); + } } - if (value == null) { - throw new IOException("no value given"); + CreateIndexResponse createIndexResponse = createIndexRequestBuilder.execute().actionGet(); + logger.info("index {} created: {}", index, createIndexResponse); + return this; + } + + + @Override + public ClientMethods newMapping(String index, String type, Map mapping) { + PutMappingRequestBuilder putMappingRequestBuilder = + new PutMappingRequestBuilder(client(), PutMappingAction.INSTANCE) + .setIndices(index) + .setType(type) + .setSource(mapping); + putMappingRequestBuilder.execute().actionGet(); + logger.info("mapping created for index {} and type {}", index, type); + return this; + } + + @Override + public ClientMethods deleteIndex(String index) { + if (closed) { + throwClose(); } - Settings.Builder updateSettingsBuilder = Settings.builder(); - updateSettingsBuilder.put(key, value.toString()); - UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(index) - .settings(updateSettingsBuilder); - client().execute(UpdateSettingsAction.INSTANCE, updateSettingsRequest).actionGet(); + if (client == null) { + logger.warn("no client"); + return this; + } + if (index == null) { + logger.warn("no index name given to delete index"); + return this; + } + DeleteIndexRequestBuilder deleteIndexRequestBuilder = + new DeleteIndexRequestBuilder(client(), DeleteIndexAction.INSTANCE, index); + deleteIndexRequestBuilder.execute().actionGet(); + return this; + } + + @Override + public ClientMethods waitForResponses(TimeValue maxWaitTime) throws InterruptedException, ExecutionException { + if (closed) { + throwClose(); + } + while (!bulkProcessor.awaitClose(maxWaitTime.getMillis(), TimeUnit.MILLISECONDS)) { + logger.warn("still waiting for responses"); + } + return this; } public void waitForRecovery() throws IOException { @@ -181,6 +555,7 @@ public abstract class AbstractClient { client().execute(RecoveryAction.INSTANCE, new RecoveryRequest()).actionGet(); } + @Override public int waitForRecovery(String index) throws IOException { if (client() == null) { return -1; @@ -195,6 +570,7 @@ public abstract class AbstractClient { return shards; } + @Override public void waitForCluster(String statusString, TimeValue timeout) throws IOException { if (client() == null) { return; @@ -283,8 +659,8 @@ public abstract class AbstractClient { if (client() == null) { return; } - if (!mappings().isEmpty()) { - for (Map.Entry me : mappings().entrySet()) { + if (!mappings.isEmpty()) { + for (Map.Entry me : mappings.entrySet()) { client().execute(PutMappingAction.INSTANCE, new PutMappingRequest(index).type(me.getKey()).source(me.getValue(), XContentType.JSON)).actionGet(); } @@ -333,25 +709,13 @@ public abstract class AbstractClient { return getFilters(getAliasesRequestBuilder.setIndices(index).execute().actionGet()); } - private Map getFilters(GetAliasesResponse getAliasesResponse) { - Map result = new HashMap<>(); - for (ObjectObjectCursor> object : getAliasesResponse.getAliases()) { - List aliasMetaDataList = object.value; - for (AliasMetaData aliasMetaData : aliasMetaDataList) { - if (aliasMetaData.filteringRequired()) { - result.put(aliasMetaData.alias(), new String(aliasMetaData.getFilter().uncompressed())); - } else { - result.put(aliasMetaData.alias(), null); - } - } - } - return result; - } + @Override public void switchAliases(String index, String concreteIndex, List extraAliases) { switchAliases(index, concreteIndex, extraAliases, null); } + @Override public void switchAliases(String index, String concreteIndex, List extraAliases, IndexAliasAdder adder) { if (client() == null) { @@ -414,6 +778,7 @@ public abstract class AbstractClient { } } + @Override public void performRetentionPolicy(String index, String concreteIndex, int timestampdiff, int mintokeep) { if (client() == null) { return; @@ -472,6 +837,7 @@ public abstract class AbstractClient { } } + @Override public Long mostRecentDocument(String index, String timestampfieldname) { if (client() == null) { return null; @@ -494,4 +860,68 @@ public abstract class AbstractClient { return null; } + @Override + public boolean hasThrowable() { + return throwable != null; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + protected static void throwClose() { + throw new ElasticsearchException("client is closed"); + } + + + protected void updateIndexSetting(String index, String key, Object value) throws IOException { + if (client() == null) { + return; + } + if (index == null) { + throw new IOException("no index name given"); + } + if (key == null) { + throw new IOException("no key given"); + } + if (value == null) { + throw new IOException("no value given"); + } + Settings.Builder updateSettingsBuilder = Settings.builder(); + updateSettingsBuilder.put(key, value.toString()); + UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(index) + .settings(updateSettingsBuilder); + client().execute(UpdateSettingsAction.INSTANCE, updateSettingsRequest).actionGet(); + } + + private Map getFilters(GetAliasesResponse getAliasesResponse) { + Map result = new HashMap<>(); + for (ObjectObjectCursor> object : getAliasesResponse.getAliases()) { + List aliasMetaDataList = object.value; + for (AliasMetaData aliasMetaData : aliasMetaDataList) { + if (aliasMetaData.filteringRequired()) { + String metaData = new String(aliasMetaData.getFilter().uncompressed(), StandardCharsets.UTF_8); + result.put(aliasMetaData.alias(), metaData); + } else { + result.put(aliasMetaData.alias(), null); + } + } + } + return result; + } + + private Settings findSettings() { + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put("host", "localhost"); + try { + String hostname = NetworkUtils.getLocalAddress().getHostName(); + logger.debug("the hostname is {}", hostname); + settingsBuilder.put("host", hostname) + .put("port", 9300); + } catch (Exception e) { + logger.warn(e.getMessage(), e); + } + return settingsBuilder.build(); + } } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkControl.java b/common/src/main/java/org/xbib/elasticsearch/client/BulkControl.java similarity index 89% rename from src/main/java/org/xbib/elasticsearch/extras/client/BulkControl.java rename to common/src/main/java/org/xbib/elasticsearch/client/BulkControl.java index 910f2f2..5fe6311 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkControl.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/BulkControl.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import java.util.Map; import java.util.Set; @@ -18,5 +18,4 @@ public interface BulkControl { Map getStartBulkRefreshIntervals(); Map getStopBulkRefreshIntervals(); - } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkMetric.java b/common/src/main/java/org/xbib/elasticsearch/client/BulkMetric.java similarity index 89% rename from src/main/java/org/xbib/elasticsearch/extras/client/BulkMetric.java rename to common/src/main/java/org/xbib/elasticsearch/client/BulkMetric.java index a45e9c2..e7a60d2 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkMetric.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/BulkMetric.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.xbib.metrics.Count; import org.xbib.metrics.Metered; @@ -27,5 +27,4 @@ public interface BulkMetric { void stop(); long elapsed(); - } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java b/common/src/main/java/org/xbib/elasticsearch/client/BulkProcessor.java similarity index 84% rename from src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java rename to common/src/main/java/org/xbib/elasticsearch/client/BulkProcessor.java index 593d9cc..7de2a3b 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/BulkProcessor.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/BulkProcessor.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BulkAction; @@ -7,13 +7,11 @@ import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.Client; +import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.EsExecutors; -import org.elasticsearch.common.util.concurrent.FutureUtils; import java.io.Closeable; import java.util.concurrent.Executors; @@ -31,9 +29,9 @@ import java.util.concurrent.atomic.AtomicLong; */ public class BulkProcessor implements Closeable { - private final int bulkActions; + private final int maximumBulkActionsPerRequest; - private final long bulkSize; + private final long maximumBulkRequestByteSize; private final ScheduledThreadPoolExecutor scheduler; @@ -41,26 +39,24 @@ public class BulkProcessor implements Closeable { private final AtomicLong executionIdGen = new AtomicLong(); - private final BulkRequestHandler bulkRequestHandler; + private final BulkExecutor bulkExecutor; private BulkRequest bulkRequest; private volatile boolean closed = false; - private BulkProcessor(Client client, Listener listener, @Nullable String name, int concurrentRequests, - int bulkActions, ByteSizeValue bulkSize, @Nullable TimeValue flushInterval) { - this.bulkActions = bulkActions; - this.bulkSize = bulkSize.getBytes(); - + private BulkProcessor(ElasticsearchClient client, Listener listener, int maximumConcurrentBulkRequests, + int maximumBulkActionsPerRequest, ByteSizeValue maximumBulkRequestByteSize, + @Nullable TimeValue flushInterval) { + this.maximumBulkActionsPerRequest = maximumBulkActionsPerRequest; + this.maximumBulkRequestByteSize = maximumBulkRequestByteSize.getBytes(); this.bulkRequest = new BulkRequest(); - this.bulkRequestHandler = concurrentRequests == 0 ? - new SyncBulkRequestHandler(client, listener) : - new AsyncBulkRequestHandler(client, listener, concurrentRequests); + this.bulkExecutor = maximumConcurrentBulkRequests == 0 ? + new SyncBulkExecutor(client, listener) : + new AsyncBulkExecutor(client, listener, maximumConcurrentBulkRequests); if (flushInterval != null) { - this.scheduler = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1, - EsExecutors.daemonThreadFactory(client.settings(), - name != null ? "[" + name + "]" : "" + "bulk_processor")); + this.scheduler = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1); this.scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); this.scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(new Flush(), flushInterval.millis(), @@ -71,7 +67,7 @@ public class BulkProcessor implements Closeable { } } - public static Builder builder(Client client, Listener listener) { + public static Builder builder(ElasticsearchClient client, Listener listener) { if (client == null) { throw new NullPointerException("The client you specified while building a BulkProcessor is null"); } @@ -111,13 +107,13 @@ public class BulkProcessor implements Closeable { } closed = true; if (this.scheduledFuture != null) { - FutureUtils.cancel(this.scheduledFuture); + this.scheduledFuture.cancel(false); this.scheduler.shutdown(); } if (bulkRequest.numberOfActions() > 0) { execute(); } - return bulkRequestHandler.awaitClose(timeout, unit); + return bulkExecutor.awaitClose(timeout, unit); } /** @@ -158,7 +154,7 @@ public class BulkProcessor implements Closeable { } /** - * Adds an {@link org.elasticsearch.action.update.UpdateRequest} to the list of actions to execute. + * Adds an {@link UpdateRequest} to the list of actions to execute. * * @param request request * @return his bulk processor @@ -184,13 +180,13 @@ public class BulkProcessor implements Closeable { private boolean isOverTheLimit() { final int count = bulkRequest.numberOfActions(); return count > 0 && - (bulkActions != -1 && count >= bulkActions) || - (bulkSize != -1 && bulkRequest.estimatedSizeInBytes() >= bulkSize); + (maximumBulkActionsPerRequest != -1 && count >= maximumBulkActionsPerRequest) || + (maximumBulkRequestByteSize != -1 && bulkRequest.estimatedSizeInBytes() >= maximumBulkRequestByteSize); } private void execute() { final BulkRequest myBulkRequest = this.bulkRequest; - bulkRequestHandler.execute(myBulkRequest, executionIdGen.incrementAndGet()); + bulkExecutor.execute(myBulkRequest, executionIdGen.incrementAndGet()); this.bulkRequest = new BulkRequest(); } @@ -245,9 +241,8 @@ public class BulkProcessor implements Closeable { */ public static class Builder { - private final Client client; + private final ElasticsearchClient client; private final Listener listener; - private String name; private int concurrentRequests = 1; private int bulkActions = 1000; private ByteSizeValue bulkSize = new ByteSizeValue(5, ByteSizeUnit.MB); @@ -260,22 +255,11 @@ public class BulkProcessor implements Closeable { * @param client the client * @param listener the listener */ - Builder(Client client, Listener listener) { + Builder(ElasticsearchClient client, Listener listener) { this.client = client; this.listener = listener; } - /** - * Sets an optional name to identify this bulk processor. - * - * @param name name - * @return this builder - */ - public Builder setName(String name) { - this.name = name; - return this; - } - /** * Sets the number of concurrent requests allowed to be executed. A value of 0 means that only a single * request will be allowed to be executed. A value of 1 means 1 concurrent request is allowed to be executed @@ -332,7 +316,7 @@ public class BulkProcessor implements Closeable { * @return a bulk processor */ public BulkProcessor build() { - return new BulkProcessor(client, listener, name, concurrentRequests, bulkActions, bulkSize, flushInterval); + return new BulkProcessor(client, listener, concurrentRequests, bulkActions, bulkSize, flushInterval); } } @@ -351,7 +335,7 @@ public class BulkProcessor implements Closeable { } } - interface BulkRequestHandler { + interface BulkExecutor { void execute(BulkRequest bulkRequest, long executionId); @@ -359,11 +343,13 @@ public class BulkProcessor implements Closeable { } - private class SyncBulkRequestHandler implements BulkRequestHandler { - private final Client client; + private static class SyncBulkExecutor implements BulkExecutor { + + private final ElasticsearchClient client; + private final BulkProcessor.Listener listener; - SyncBulkRequestHandler(Client client, BulkProcessor.Listener listener) { + SyncBulkExecutor(ElasticsearchClient client, BulkProcessor.Listener listener) { this.client = client; this.listener = listener; } @@ -389,13 +375,17 @@ public class BulkProcessor implements Closeable { } } - private class AsyncBulkRequestHandler implements BulkRequestHandler { - private final Client client; + private static class AsyncBulkExecutor implements BulkExecutor { + + private final ElasticsearchClient client; + private final BulkProcessor.Listener listener; + private final Semaphore semaphore; + private final int concurrentRequests; - private AsyncBulkRequestHandler(Client client, BulkProcessor.Listener listener, int concurrentRequests) { + private AsyncBulkExecutor(ElasticsearchClient client, BulkProcessor.Listener listener, int concurrentRequests) { this.client = client; this.listener = listener; this.concurrentRequests = concurrentRequests; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/ClientBuilder.java b/common/src/main/java/org/xbib/elasticsearch/client/ClientBuilder.java similarity index 55% rename from src/main/java/org/xbib/elasticsearch/extras/client/ClientBuilder.java rename to common/src/main/java/org/xbib/elasticsearch/client/ClientBuilder.java index 4089249..441c8e2 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/ClientBuilder.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/ClientBuilder.java @@ -1,12 +1,14 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.xbib.elasticsearch.extras.client.node.BulkNodeClient; -import org.xbib.elasticsearch.extras.client.transport.BulkTransportClient; -import org.xbib.elasticsearch.extras.client.transport.MockTransportClient; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; /** * @@ -15,12 +17,25 @@ public final class ClientBuilder implements Parameters { private final Settings.Builder settingsBuilder; + private Map, ClientMethods> clientMethodsMap; + private BulkMetric metric; private BulkControl control; public ClientBuilder() { - settingsBuilder = Settings.builder(); + this(Thread.currentThread().getContextClassLoader()); + } + + public ClientBuilder(ClassLoader classLoader) { + this.settingsBuilder = Settings.builder(); + //settingsBuilder.put("node.name", "clientnode"); + this.clientMethodsMap = new HashMap<>(); + ServiceLoader serviceLoader = ServiceLoader.load(ClientMethods.class, + classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader()); + for (ClientMethods clientMethods : serviceLoader) { + clientMethodsMap.put(clientMethods.getClass(), clientMethods); + } } public static ClientBuilder builder() { @@ -72,34 +87,18 @@ public final class ClientBuilder implements Parameters { return this; } - public BulkNodeClient toBulkNodeClient(Client client) { - Settings settings = settingsBuilder.build(); - return new BulkNodeClient() - .maxActionsPerRequest(settings.getAsInt(MAX_ACTIONS_PER_REQUEST, DEFAULT_MAX_ACTIONS_PER_REQUEST)) - .maxConcurrentRequests(settings.getAsInt(MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_REQUESTS)) - .maxVolumePerRequest(settings.getAsBytesSize(MAX_VOLUME_PER_REQUEST, DEFAULT_MAX_VOLUME_PER_REQUEST)) - .flushIngestInterval(settings.getAsTime(FLUSH_INTERVAL, DEFAULT_FLUSH_INTERVAL)) - .init(client, metric, control); + public C getClient(Class clientClass) throws IOException { + return getClient(null, clientClass); } - public BulkTransportClient toBulkTransportClient() { + @SuppressWarnings("unchecked") + public C getClient(Client client, Class clientClass) throws IOException { Settings settings = settingsBuilder.build(); - return new BulkTransportClient() + return (C) clientMethodsMap.get(clientClass) .maxActionsPerRequest(settings.getAsInt(MAX_ACTIONS_PER_REQUEST, DEFAULT_MAX_ACTIONS_PER_REQUEST)) .maxConcurrentRequests(settings.getAsInt(MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_REQUESTS)) .maxVolumePerRequest(settings.getAsBytesSize(MAX_VOLUME_PER_REQUEST, DEFAULT_MAX_VOLUME_PER_REQUEST)) .flushIngestInterval(settings.getAsTime(FLUSH_INTERVAL, DEFAULT_FLUSH_INTERVAL)) - .init(settings, metric, control); + .init(client, settings, metric, control); } - - public MockTransportClient toMockTransportClient() { - Settings settings = settingsBuilder.build(); - return new MockTransportClient() - .maxActionsPerRequest(settings.getAsInt(MAX_ACTIONS_PER_REQUEST, DEFAULT_MAX_ACTIONS_PER_REQUEST)) - .maxConcurrentRequests(settings.getAsInt(MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_CONCURRENT_REQUESTS)) - .maxVolumePerRequest(settings.getAsBytesSize(MAX_VOLUME_PER_REQUEST, DEFAULT_MAX_VOLUME_PER_REQUEST)) - .flushIngestInterval(settings.getAsTime(FLUSH_INTERVAL, DEFAULT_FLUSH_INTERVAL)) - .init(settings, metric, control); - } - } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/ClientMethods.java b/common/src/main/java/org/xbib/elasticsearch/client/ClientMethods.java similarity index 83% rename from src/main/java/org/xbib/elasticsearch/extras/client/ClientMethods.java rename to common/src/main/java/org/xbib/elasticsearch/client/ClientMethods.java index 74de495..f98ec67 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/ClientMethods.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/ClientMethods.java @@ -1,9 +1,10 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; @@ -19,45 +20,49 @@ import java.util.concurrent.ExecutionException; */ public interface ClientMethods extends Parameters { - /** - * Initialize new ingest client, wrap an existing Elasticsearch client, and set up metrics. - * - * @param client the Elasticsearch client - * @param metric metric - * @param control control - * @return this ingest - * @throws IOException if client could not get created - */ - ClientMethods init(ElasticsearchClient client, BulkMetric metric, BulkControl control) throws IOException; + ClientMethods init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control); /** - * Initialize, create new ingest client, and set up metrics. + * Return Elasticsearch client. * - * @param settings settings - * @param metric metric - * @param control control - * @return this ingest - * @throws IOException if client could not get created + * @return Elasticsearch client */ - ClientMethods init(Settings settings, BulkMetric metric, BulkControl control) throws IOException; + ElasticsearchClient client(); /** - * Return Elasticsearch client. + * Bulked index request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. * - * @return Elasticsearch client + * @param index the index + * @param type the type + * @param id the id + * @param create true if document must be created + * @param source the source + * @return this */ - ElasticsearchClient client(); + ClientMethods index(String index, String type, String id, boolean create, BytesReference source); /** - * Index document. + * Bulked index request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. * * @param index the index * @param type the type * @param id the id + * @param create true if document must be created * @param source the source * @return this */ - ClientMethods index(String index, String type, String id, String source); + ClientMethods index(String index, String type, String id, boolean create, String source); + + /** + * Bulked index request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. + * + * @param indexRequest the index request to add + * @return this ingest + */ + ClientMethods indexRequest(IndexRequest indexRequest); /** * Delete document. @@ -70,7 +75,31 @@ public interface ClientMethods extends Parameters { ClientMethods delete(String index, String type, String id); /** - * Update document. Use with precaution! Does not work in all cases. + * Bulked delete request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. + * + * @param deleteRequest the delete request to add + * @return this ingest + */ + ClientMethods deleteRequest(DeleteRequest deleteRequest); + + /** + * Bulked update request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. + * Note that updates only work correctly when all operations between nodes are synchronized. + * + * @param index the index + * @param type the type + * @param id the id + * @param source the source + * @return this + */ + ClientMethods update(String index, String type, String id, BytesReference source); + + /** + * Bulked update request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. + * Note that updates only work correctly when all operations between nodes are synchronized. * * @param index the index * @param type the type @@ -80,6 +109,16 @@ public interface ClientMethods extends Parameters { */ ClientMethods update(String index, String type, String id, String source); + /** + * Bulked update request. Each request will be added to a queue for bulking requests. + * Submitting request will be done when bulk limits are exceeded. + * Note that updates only work correctly when all operations between nodes are synchronized. + * + * @param updateRequest the update request to add + * @return this ingest + */ + ClientMethods updateRequest(UpdateRequest updateRequest); + /** * Set the maximum number of actions per request. * @@ -205,34 +244,6 @@ public interface ClientMethods extends Parameters { */ ClientMethods stopBulk(String index) throws IOException; - /** - * Bulked index request. Each request will be added to a queue for bulking requests. - * Submitting request will be done when bulk limits are exceeded. - * - * @param indexRequest the index request to add - * @return this ingest - */ - ClientMethods bulkIndex(IndexRequest indexRequest); - - /** - * Bulked delete request. Each request will be added to a queue for bulking requests. - * Submitting request will be done when bulk limits are exceeded. - * - * @param deleteRequest the delete request to add - * @return this ingest - */ - ClientMethods bulkDelete(DeleteRequest deleteRequest); - - /** - * Bulked update request. Each request will be added to a queue for bulking requests. - * Submitting request will be done when bulk limits are exceeded. - * Note that updates only work correctly when all operations between nodes are synchronized! - * - * @param updateRequest the update request to add - * @return this ingest - */ - ClientMethods bulkUpdate(UpdateRequest updateRequest); - /** * Flush ingest, move all pending documents to the cluster. * @@ -388,5 +399,5 @@ public interface ClientMethods extends Parameters { /** * Shutdown the ingesting. */ - void shutdown(); + void shutdown() throws IOException; } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/IndexAliasAdder.java b/common/src/main/java/org/xbib/elasticsearch/client/IndexAliasAdder.java similarity index 84% rename from src/main/java/org/xbib/elasticsearch/extras/client/IndexAliasAdder.java rename to common/src/main/java/org/xbib/elasticsearch/client/IndexAliasAdder.java index a659ab4..7c93d22 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/IndexAliasAdder.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/IndexAliasAdder.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java b/common/src/main/java/org/xbib/elasticsearch/client/NetworkUtils.java similarity index 98% rename from src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java rename to common/src/main/java/org/xbib/elasticsearch/client/NetworkUtils.java index 277e13a..847e414 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/NetworkUtils.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/NetworkUtils.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -11,6 +11,7 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.Locale; @@ -234,7 +235,7 @@ public class NetworkUtils { } private static void sortInterfaces(List interfaces) { - Collections.sort(interfaces, (o1, o2) -> Integer.compare(o1.getIndex(), o2.getIndex())); + Collections.sort(interfaces, Comparator.comparingInt(NetworkInterface::getIndex)); } private static void sortAddresses(List addressList) { diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/Parameters.java b/common/src/main/java/org/xbib/elasticsearch/client/Parameters.java similarity index 94% rename from src/main/java/org/xbib/elasticsearch/extras/client/Parameters.java rename to common/src/main/java/org/xbib/elasticsearch/client/Parameters.java index 41cc6d2..3d5f818 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/Parameters.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/Parameters.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; @@ -24,5 +24,4 @@ public interface Parameters { String MAX_VOLUME_PER_REQUEST = "max_volume_per_request"; String FLUSH_INTERVAL = "flush_interval"; - } diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkControl.java b/common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkControl.java similarity index 96% rename from src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkControl.java rename to common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkControl.java index b9a92d6..b8257f7 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkControl.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkControl.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import java.util.HashMap; import java.util.HashSet; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkMetric.java b/common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkMetric.java similarity index 53% rename from src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkMetric.java rename to common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkMetric.java index e836816..9b82444 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/SimpleBulkMetric.java +++ b/common/src/main/java/org/xbib/elasticsearch/client/SimpleBulkMetric.java @@ -1,32 +1,53 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client; import org.xbib.metrics.Count; import org.xbib.metrics.CountMetric; import org.xbib.metrics.Meter; import org.xbib.metrics.Metered; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + /** * */ public class SimpleBulkMetric implements BulkMetric { - private final Meter totalIngest = new Meter(); + private final ScheduledExecutorService executorService; - private final Count totalIngestSizeInBytes = new CountMetric(); + private final Meter totalIngest; - private final Count currentIngest = new CountMetric(); + private final Count totalIngestSizeInBytes; - private final Count currentIngestNumDocs = new CountMetric(); + private final Count currentIngest; - private final Count submitted = new CountMetric(); + private final Count currentIngestNumDocs; - private final Count succeeded = new CountMetric(); + private final Count submitted; - private final Count failed = new CountMetric(); + private final Count succeeded; + + private final Count failed; private Long started; private Long stopped; + public SimpleBulkMetric() { + this(Executors.newSingleThreadScheduledExecutor()); + } + + public SimpleBulkMetric(ScheduledExecutorService executorService) { + this.executorService = executorService; + totalIngest = new Meter(executorService); + totalIngestSizeInBytes = new CountMetric(); + currentIngest = new CountMetric(); + currentIngestNumDocs = new CountMetric(); + submitted = new CountMetric(); + succeeded = new CountMetric(); + failed = new CountMetric(); + } + @Override public Metered getTotalIngest() { return totalIngest; @@ -65,13 +86,14 @@ public class SimpleBulkMetric implements BulkMetric { @Override public void start() { this.started = System.nanoTime(); - totalIngest.spawn(5L); + totalIngest.start(5L); } @Override public void stop() { this.stopped = System.nanoTime(); totalIngest.stop(); + executorService.shutdownNow(); } @Override diff --git a/common/src/main/java/org/xbib/elasticsearch/client/package-info.java b/common/src/main/java/org/xbib/elasticsearch/client/package-info.java new file mode 100644 index 0000000..941a500 --- /dev/null +++ b/common/src/main/java/org/xbib/elasticsearch/client/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Elasticsearch client. + */ +package org.xbib.elasticsearch.client; diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java b/common/src/test/java/org/xbib/elasticsearch/client/common/AliasTests.java similarity index 73% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java rename to common/src/test/java/org/xbib/elasticsearch/client/common/AliasTests.java index 81a84d9..37db0de 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/AliasTest.java +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/AliasTests.java @@ -1,7 +1,4 @@ -package org.xbib.elasticsearch.extras.client; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +package org.xbib.elasticsearch.client.common; import com.carrotsearch.hppc.cursors.ObjectCursor; import org.apache.logging.log4j.LogManager; @@ -13,10 +10,8 @@ import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequestBuilder import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.common.Strings; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; +import org.elasticsearch.test.ESSingleNodeTestCase; -import java.io.IOException; import java.util.Collections; import java.util.Iterator; import java.util.Set; @@ -24,49 +19,44 @@ import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * - */ -public class AliasTest extends NodeTestBase { +public class AliasTests extends ESSingleNodeTestCase { - private static final Logger logger = LogManager.getLogger(AliasTest.class.getName()); + private static final Logger logger = LogManager.getLogger(AliasTests.class.getName()); - @Test - public void testAlias() throws IOException { + public void testAlias() { CreateIndexRequest indexRequest = new CreateIndexRequest("test"); - client("1").admin().indices().create(indexRequest).actionGet(); + client().admin().indices().create(indexRequest).actionGet(); // put alias IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add() .index("test").alias("test_alias") ); - client("1").admin().indices().aliases(indicesAliasesRequest).actionGet(); + client().admin().indices().aliases(indicesAliasesRequest).actionGet(); // get alias GetAliasesRequest getAliasesRequest = new GetAliasesRequest(Strings.EMPTY_ARRAY); long t0 = System.nanoTime(); - GetAliasesResponse getAliasesResponse = client("1").admin().indices().getAliases(getAliasesRequest).actionGet(); + GetAliasesResponse getAliasesResponse = client().admin().indices().getAliases(getAliasesRequest).actionGet(); long t1 = (System.nanoTime() - t0) / 1000000; logger.info("{} time(ms) = {}", getAliasesResponse.getAliases(), t1); assertTrue(t1 >= 0); } - @Test - public void testMostRecentIndex() throws IOException { + public void testMostRecentIndex() { String alias = "test"; CreateIndexRequest indexRequest = new CreateIndexRequest("test20160101"); - client("1").admin().indices().create(indexRequest).actionGet(); + client().admin().indices().create(indexRequest).actionGet(); indexRequest = new CreateIndexRequest("test20160102"); - client("1").admin().indices().create(indexRequest).actionGet(); + client().admin().indices().create(indexRequest).actionGet(); indexRequest = new CreateIndexRequest("test20160103"); - client("1").admin().indices().create(indexRequest).actionGet(); + client().admin().indices().create(indexRequest).actionGet(); IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest(); indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add() .indices("test20160101", "test20160102", "test20160103") .alias(alias) ); - client("1").admin().indices().aliases(indicesAliasesRequest).actionGet(); + client().admin().indices().aliases(indicesAliasesRequest).actionGet(); - GetAliasesRequestBuilder getAliasesRequestBuilder = new GetAliasesRequestBuilder(client("1"), + GetAliasesRequestBuilder getAliasesRequestBuilder = new GetAliasesRequestBuilder(client(), GetAliasesAction.INSTANCE); GetAliasesResponse getAliasesResponse = getAliasesRequestBuilder.setAliases(alias).execute().actionGet(); Pattern pattern = Pattern.compile("^(.*?)(\\d+)$"); @@ -83,5 +73,4 @@ public class AliasTest extends NodeTestBase { assertEquals("test20160101", it.next()); logger.info("result={}", result); } - } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/NetworkTest.java b/common/src/test/java/org/xbib/elasticsearch/client/common/NetworkTest.java similarity index 81% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/NetworkTest.java rename to common/src/test/java/org/xbib/elasticsearch/client/common/NetworkTest.java index b9e7a87..0ed4fc8 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/NetworkTest.java +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/NetworkTest.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client.common; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -9,18 +9,21 @@ import java.net.NetworkInterface; import java.util.Collections; import java.util.Enumeration; -/** - * - */ public class NetworkTest { private static final Logger logger = LogManager.getLogger(NetworkTest.class); + /** + * Demonstrates the slowness oj Java network interface lookup on certain environments. + * May be a killer for ES node startup - so avoid automatic traversal of NICs at all costs. + * + * @throws Exception if test fails + */ @Test public void testNetwork() throws Exception { Enumeration nets = NetworkInterface.getNetworkInterfaces(); for (NetworkInterface netint : Collections.list(nets)) { - System.out.println("checking network interface = " + netint.getName()); + logger.info("checking network interface = " + netint.getName()); Enumeration inetAddresses = netint.getInetAddresses(); for (InetAddress addr : Collections.list(inetAddresses)) { logger.info("found address = " + addr.getHostAddress() diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java b/common/src/test/java/org/xbib/elasticsearch/client/common/SearchTests.java similarity index 73% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java rename to common/src/test/java/org/xbib/elasticsearch/client/common/SearchTests.java index 93a7ca7..09771c5 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SearchTest.java +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/SearchTests.java @@ -1,8 +1,4 @@ -package org.xbib.elasticsearch.extras.client; - -import static org.elasticsearch.client.Requests.indexRequest; -import static org.elasticsearch.client.Requests.refreshRequest; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +package org.xbib.elasticsearch.client.common; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -10,29 +6,24 @@ import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.client.Client; +import org.elasticsearch.client.Requests; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortOrder; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; +import org.elasticsearch.test.ESSingleNodeTestCase; -/** - * - */ -public class SearchTest extends NodeTestBase { +public class SearchTests extends ESSingleNodeTestCase { - private static final Logger logger = LogManager.getLogger(SearchTest.class.getName()); + private static final Logger logger = LogManager.getLogger(SearchTests.class.getName()); - @Test public void testSearch() throws Exception { - Client client = client("1"); long t0 = System.currentTimeMillis(); - BulkRequestBuilder builder = new BulkRequestBuilder(client, BulkAction.INSTANCE); + BulkRequestBuilder builder = new BulkRequestBuilder(client(), BulkAction.INSTANCE); for (int i = 0; i < 1000; i++) { - builder.add(indexRequest() + builder.add(Requests.indexRequest() .index("pages").type("row") - .source(jsonBuilder() + .source(XContentFactory.jsonBuilder() .startObject() .field("user1", "kimchy") .field("user2", "kimchy") @@ -47,18 +38,15 @@ public class SearchTest extends NodeTestBase { .field("rs", 1234) .endObject())); } - client.bulk(builder.request()).actionGet(); - - client.admin().indices().refresh(refreshRequest()).actionGet(); - + client().bulk(builder.request()).actionGet(); + client().admin().indices().refresh(Requests.refreshRequest()).actionGet(); long t1 = System.currentTimeMillis(); logger.info("t1-t0 = {}", t1 - t0); - for (int i = 0; i < 100; i++) { t1 = System.currentTimeMillis(); QueryBuilder queryStringBuilder = QueryBuilders.queryStringQuery("rs:" + 1234); - SearchRequestBuilder requestBuilder = client.prepareSearch() + SearchRequestBuilder requestBuilder = client().prepareSearch() .setIndices("pages") .setTypes("row") .setQuery(queryStringBuilder) diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java b/common/src/test/java/org/xbib/elasticsearch/client/common/SimpleTests.java similarity index 67% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java rename to common/src/test/java/org/xbib/elasticsearch/client/common/SimpleTests.java index c3fe074..8b89df6 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/SimpleTest.java +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/SimpleTests.java @@ -1,9 +1,7 @@ -package org.xbib.elasticsearch.extras.client; - -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.index.query.QueryBuilders.matchQuery; -import static org.junit.Assert.assertEquals; +package org.xbib.elasticsearch.client.common; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; @@ -11,49 +9,47 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.support.WriteRequest; -import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.test.ESSingleNodeTestCase; + +public class SimpleTests extends ESSingleNodeTestCase { -/** - * - */ -public class SimpleTest extends NodeTestBase { + private static final Logger logger = LogManager.getLogger(SimpleTests.class.getName()); - @Test public void test() throws Exception { - Client client = client("1"); try { DeleteIndexRequestBuilder deleteIndexRequestBuilder = - new DeleteIndexRequestBuilder(client, DeleteIndexAction.INSTANCE, "test"); + new DeleteIndexRequestBuilder(client(), DeleteIndexAction.INSTANCE, "test"); deleteIndexRequestBuilder.execute().actionGet(); } catch (Exception e) { - // ignore + logger.warn(e.getMessage(), e); } - CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client, + CreateIndexRequestBuilder createIndexRequestBuilder = new CreateIndexRequestBuilder(client(), CreateIndexAction.INSTANCE) .setIndex("test") .setSettings(Settings.builder() .put("index.analysis.analyzer.default.filter.0", "lowercase") - .put("index.analysis.analyzer.default.filter.1", "trim") + // where is the trim token filter??? + //.put("index.analysis.analyzer.default.filter.1", "trim") .put("index.analysis.analyzer.default.tokenizer", "keyword") .build()); createIndexRequestBuilder.execute().actionGet(); - IndexRequestBuilder indexRequestBuilder = new IndexRequestBuilder(client, IndexAction.INSTANCE); + IndexRequestBuilder indexRequestBuilder = new IndexRequestBuilder(client(), IndexAction.INSTANCE); indexRequestBuilder .setIndex("test") .setType("test") .setId("1") - .setSource(jsonBuilder().startObject().field("field", + .setSource(XContentFactory.jsonBuilder().startObject().field("field", "1%2fPJJP3JV2C24iDfEu9XpHBaYxXh%2fdHTbmchB35SDznXO2g8Vz4D7GTIvY54iMiX_149c95f02a8").endObject()) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .execute() .actionGet(); - String doc = client.prepareSearch("test") + String doc = client().prepareSearch("test") .setTypes("test") - .setQuery(matchQuery("field", + .setQuery(QueryBuilders.matchQuery("field", "1%2fPJJP3JV2C24iDfEu9XpHBaYxXh%2fdHTbmchB35SDznXO2g8Vz4D7GTIvY54iMiX_149c95f02a8")) .execute() .actionGet() diff --git a/common/src/test/java/org/xbib/elasticsearch/client/common/WildcardTests.java b/common/src/test/java/org/xbib/elasticsearch/client/common/WildcardTests.java new file mode 100644 index 0000000..bb84bb9 --- /dev/null +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/WildcardTests.java @@ -0,0 +1,51 @@ +package org.xbib.elasticsearch.client.common; + +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Requests; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.test.ESSingleNodeTestCase; + +import java.io.IOException; + +public class WildcardTests extends ESSingleNodeTestCase { + + public void testWildcard() throws Exception { + index("1", "010"); + index("2", "0*0"); + // exact + validateCount(QueryBuilders.queryStringQuery("010").defaultField("field"), 1); + validateCount(QueryBuilders.queryStringQuery("0\\*0").defaultField("field"), 1); + // pattern + validateCount(QueryBuilders.queryStringQuery("0*0").defaultField("field"), 1); // 2? + validateCount(QueryBuilders.queryStringQuery("0?0").defaultField("field"), 1); // 2? + validateCount(QueryBuilders.queryStringQuery("0**0").defaultField("field"), 1); // 2? + validateCount(QueryBuilders.queryStringQuery("0??0").defaultField("field"), 0); + validateCount(QueryBuilders.queryStringQuery("*10").defaultField("field"), 1); + validateCount(QueryBuilders.queryStringQuery("*1*").defaultField("field"), 1); + validateCount(QueryBuilders.queryStringQuery("*\\*0").defaultField("field"), 0); // 1? + validateCount(QueryBuilders.queryStringQuery("*\\**").defaultField("field"), 0); // 1? + } + + private void index(String id, String fieldValue) throws IOException { + client().index(Requests.indexRequest() + .index("index").type("type").id(id) + .source(XContentFactory.jsonBuilder().startObject().field("field", fieldValue).endObject()) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)) + .actionGet(); + } + + private void validateCount(QueryBuilder queryBuilder, long expectedHits) { + final long actualHits = count(queryBuilder); + if (actualHits != expectedHits) { + throw new RuntimeException("actualHits=" + actualHits + ", expectedHits=" + expectedHits); + } + } + + private long count(QueryBuilder queryBuilder) { + return client().prepareSearch("index").setTypes("type") + .setQuery(queryBuilder) + .execute().actionGet().getHits().getTotalHits(); + } +} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/package-info.java b/common/src/test/java/org/xbib/elasticsearch/client/common/package-info.java similarity index 52% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/package-info.java rename to common/src/test/java/org/xbib/elasticsearch/client/common/package-info.java index 2bfc45c..af3209f 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/package-info.java +++ b/common/src/test/java/org/xbib/elasticsearch/client/common/package-info.java @@ -1,4 +1,4 @@ /** * Classes to test Elasticsearch clients. */ -package org.xbib.elasticsearch.extras.client; +package org.xbib.elasticsearch.client.common; diff --git a/src/integration-test/resources/log4j2.xml b/common/src/test/resources/log4j2.xml similarity index 100% rename from src/integration-test/resources/log4j2.xml rename to common/src/test/resources/log4j2.xml diff --git a/gradle.properties b/gradle.properties index a739c74..f75801d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,28 @@ -group = org.xbib -name = elasticsearch-extras-client -version = 5.3.0.1 - -elasticsearch-client-transport.version = 5.3.0 -xbib-metrics.version = 1.0.0 -netty-transport-native-epoll.version = 4.1.7.Final -log4j.version = 2.8 +org.gradle.daemon=false +org.gradle.warning.mode=all + +group = org.xbib.elasticsearch +name = elasticsearch-client-netty +version = 6.2.2.0 + +elasticsearch.version = 6.2.2 +netty.version = 4.1.24.Final +tcnative.version = 2.0.7.Final +alpnagent.version = 2.0.7 +#xbib-netty-http-client.version = 4.1.16.1 +xbib-netty-http-client.version = 4.1.24.0 +xbib-metrics.version = 1.1.0 + +# elasticsearch build plugin +xbib-elasticsearch-test.version = 6.2.2.0 +lucene.version = 7.2.1 +spatial4j.version = 0.6 +jts.version = 1.13 +jna.version = 4.5.1 + +# test +log4j.version = 2.9.1 junit.version = 4.12 -wagon.version = 2.10 +wagon.version = 3.0.0 +asciidoclet.version = 1.6.0.0 + diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..2318dcd --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,8 @@ +ext { + user = 'xbib' + name = 'elasticsearch-java-client' + description = 'Netty Java client for Elasticsearch' + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 0337849..c05a223 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -6,7 +6,7 @@ task xbibUpload(type: Upload) { if (project.hasProperty("xbibUsername")) { mavenDeployer { configuration = configurations.wagon - repository(url: 'scpexe://xbib.org/repository') { + repository(url: 'sftp://xbib.org/repository') { authentication(userName: xbibUsername, privateKey: xbibPrivateKey) } } diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle index 5de408d..d759e4c 100644 --- a/gradle/sonarqube.gradle +++ b/gradle/sonarqube.gradle @@ -1,8 +1,8 @@ tasks.withType(FindBugs) { ignoreFailures = true reports { - xml.enabled = true - html.enabled = false + xml.enabled = false + html.enabled = true } } tasks.withType(Pmd) { @@ -20,22 +20,11 @@ tasks.withType(Checkstyle) { } } -jacocoTestReport { - reports { - xml.enabled true - csv.enabled false - xml.destination "${buildDir}/reports/jacoco-xml" - html.destination "${buildDir}/reports/jacoco-html" - } -} - sonarqube { properties { property "sonar.projectName", "${project.group} ${project.name}" property "sonar.sourceEncoding", "UTF-8" - property "sonar.tests", "src/integration-test/java" property "sonar.scm.provider", "git" - property "sonar.java.coveragePlugin", "jacoco" property "sonar.junit.reportsPath", "build/test-results/test/" } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 51288f9c2f05faf8d42e1a751a387ca7923882c3..91ca28c8b802289c3a438766657a5e98f20eff03 100644 GIT binary patch delta 34833 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 delta 34775 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

wnDjYg2i+e`iOtVRs(`1* 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d56c0fe..c2bee35 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Dec 04 10:06:25 CET 2016 +#Fri May 04 14:40:54 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip diff --git a/gradlew b/gradlew index 4453cce..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/http/build.gradle b/http/build.gradle new file mode 100644 index 0000000..0481477 --- /dev/null +++ b/http/build.gradle @@ -0,0 +1,63 @@ +buildscript { + repositories { + jcenter() + maven { + url 'http://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.2.2.0" + } +} + +apply plugin: 'org.xbib.gradle.plugin.elasticsearch.build' + +configurations { + main + tests +} + +dependencies { + compile project(':common') + compile "org.xbib:netty-http-client:${project.property('xbib-netty-http-client.version')}" + testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" + testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" +} + +jar { + baseName "${rootProject.name}-common" +} + +task testJar(type: Jar, dependsOn: testClasses) { + baseName = "${project.archivesBaseName}-tests" + from sourceSets.test.output +} + +artifacts { + main jar + tests testJar + archives sourcesJar, javadocJar +} + +test { + enabled = true + include '**/SimpleTest.*' + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} + +randomizedTest { + enabled = false +} + +esTest { + enabled = true + // test with the jars, not the classes, for security manager + classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files + systemProperty 'tests.security.manager', 'true' + // maybe we like some extra security policy for our code + systemProperty 'tests.security.policy', '/extra-security.policy' +} +esTest.dependsOn jar, testJar diff --git a/http/config/checkstyle/checkstyle.xml b/http/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7af8d6d --- /dev/null +++ b/http/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/http/src/docs/asciidoc/css/foundation.css b/http/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/http/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/http/src/docs/asciidoclet/overview.adoc b/http/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..7947331 --- /dev/null +++ b/http/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Elasticsearch Java client +Jörg Prante +Version 5.4.0.0 + diff --git a/http/src/main/java/org/elasticsearch/action/admin/cluster/node/info/HttpNodesInfoAction.java b/http/src/main/java/org/elasticsearch/action/admin/cluster/node/info/HttpNodesInfoAction.java new file mode 100644 index 0000000..bb725cc --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/admin/cluster/node/info/HttpNodesInfoAction.java @@ -0,0 +1,164 @@ +package org.elasticsearch.action.admin.cluster.node.info; + +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.http.HttpInfo; +import org.elasticsearch.ingest.IngestInfo; +import org.elasticsearch.monitor.jvm.JvmInfo; +import org.elasticsearch.monitor.os.OsInfo; +import org.elasticsearch.monitor.process.ProcessInfo; +import org.elasticsearch.threadpool.ThreadPoolInfo; +import org.elasticsearch.transport.TransportInfo; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.elasticsearch.client.http.HttpActionContext; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + */ +public class HttpNodesInfoAction extends HttpAction { + + @Override + public NodesInfoAction getActionInstance() { + return NodesInfoAction.INSTANCE; + } + + /** + * Endpoint "/_nodes/{nodeId}/{metrics}" + * + * @param url url + * @param request request + * @return HTTP request + */ + @Override + protected RequestBuilder createHttpRequest(String url, NodesInfoRequest request) { + StringBuilder path = new StringBuilder("/_nodes"); + if (request.nodesIds() != null) { + String nodeIds = String.join(",", request.nodesIds()); + if (nodeIds.length() > 0) { + path.append("/").append(nodeIds); + } + } else { + path.append("/_all"); + } + List metrics = new LinkedList<>(); + if (request.http()) { + metrics.add("http"); + } + if (request.jvm()) { + metrics.add("jvm"); + } + if (request.os()) { + metrics.add("os"); + } + if (request.plugins()) { + metrics.add("plugins"); + } + if (request.process()) { + metrics.add("process"); + } + if (request.settings()) { + metrics.add("settings"); + } + if (request.threadPool()) { + metrics.add("thread_pool"); + } + if (request.transport()) { + metrics.add("transport"); + } + if (!metrics.isEmpty()) { + path.append("/").append(String.join(",", metrics)); + } + return newGetRequest(url, path.toString()); + } + + @Override + protected CheckedFunction entityParser() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + protected NodesInfoResponse createResponse(HttpActionContext httpContext) { + Map map = null; + + String string = (String)map.get("cluster_name"); + ClusterName clusterName = new ClusterName(string); + List nodeInfoList = new LinkedList<>(); + map = (Map)map.get("nodes"); + for (Map.Entry entry : map.entrySet()) { + String nodeId = entry.getKey(); + String ephemeralId = null; + Map map2 = (Map) entry.getValue(); + String nodeName = (String)map2.get("name"); + String hostName = (String)map2.get("host"); + String hostAddress = (String)map2.get("ip"); + // [/][:] + String transportAddressString = (String)map2.get("transport_address"); + int pos = transportAddressString.indexOf(':'); + String host = pos > 0 ? transportAddressString.substring(0, pos) : transportAddressString; + int port = Integer.parseInt(pos > 0 ? transportAddressString.substring(pos + 1) : "0"); + pos = host.indexOf('/'); + host = pos > 0 ? host.substring(0, pos) : host; + try { + InetAddress[] inetAddresses = InetAddress.getAllByName(host); + TransportAddress transportAddress = new TransportAddress(inetAddresses[0], port); + Build build = new Build((String) map2.get("build"), (String)map2.get("date"), (Boolean)map2.get("snapshst")); + Map attributes = Collections.emptyMap(); + Set roles = new HashSet<>(); + Version version = Version.fromString((String) map2.get("version")); + DiscoveryNode discoveryNode = new DiscoveryNode(nodeName, nodeId, ephemeralId, hostName, hostAddress, + transportAddress, + attributes, roles, version); + /*Map settingsMap = map2.containsKey("settings") ? + XContentHelper. + SettingsLoader.Helper.loadNestedFromMap((Map) map2.get("settings")) : + Collections.emptyMap(); + + Settings settings = Settings.builder() + + .put(settingsMap) + .build();*/ + OsInfo os = null; + ProcessInfo processInfo = null; + JvmInfo jvmInfo = null; + ThreadPoolInfo threadPoolInfo = null; + TransportInfo transportInfo = null; + HttpInfo httpInfo = null; + PluginsAndModules pluginsAndModules = null; + IngestInfo ingestInfo = null; + ByteSizeValue totalIndexingBuffer = null; + NodeInfo nodeInfo = new NodeInfo(version, + build, + discoveryNode, + //serviceAttributes, + //settings, + null, + os, processInfo, jvmInfo, threadPoolInfo, transportInfo, httpInfo, pluginsAndModules, + ingestInfo, + totalIndexingBuffer); + nodeInfoList.add(nodeInfo); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + List failures = null; + return new NodesInfoResponse(clusterName, nodeInfoList, failures); + } +} diff --git a/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java b/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java new file mode 100644 index 0000000..971e1c7 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java @@ -0,0 +1,48 @@ +package org.elasticsearch.action.admin.cluster.settings; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * + */ +public class HttpClusterUpdateSettingsAction extends HttpAction { + + @Override + public ClusterUpdateSettingsAction getActionInstance() { + return ClusterUpdateSettingsAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, ClusterUpdateSettingsRequest request) { + try { + XContentBuilder builder = jsonBuilder(); + builder.startObject().startObject("persistent"); + request.persistentSettings().toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + builder.startObject("transient"); + request.transientSettings().toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject().endObject(); + return newPutRequest(url, "/_cluster/settings", builder.bytes()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + protected CheckedFunction entityParser() { + return parser -> { + // TODO(jprante) + return new ClusterUpdateSettingsResponse(); + }; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/admin/indices/create/HttpCreateIndexAction.java b/http/src/main/java/org/elasticsearch/action/admin/indices/create/HttpCreateIndexAction.java new file mode 100644 index 0000000..428b84f --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/admin/indices/create/HttpCreateIndexAction.java @@ -0,0 +1,35 @@ +package org.elasticsearch.action.admin.indices.create; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +public class HttpCreateIndexAction extends HttpAction { + + @Override + public CreateIndexAction getActionInstance() { + return CreateIndexAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, CreateIndexRequest createIndexRequest) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder = createIndexRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); + return newPutRequest(url, "/" + createIndexRequest.index(), builder.bytes()); + } + + @Override + protected CheckedFunction entityParser() { + return parser -> { + // TODO(jprante) build real create index response + return new CreateIndexResponse(); + }; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/admin/indices/refresh/HttpRefreshIndexAction.java b/http/src/main/java/org/elasticsearch/action/admin/indices/refresh/HttpRefreshIndexAction.java new file mode 100644 index 0000000..88f76ea --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/admin/indices/refresh/HttpRefreshIndexAction.java @@ -0,0 +1,30 @@ +package org.elasticsearch.action.admin.indices.refresh; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + * + */ +public class HttpRefreshIndexAction extends HttpAction { + + @Override + public RefreshAction getActionInstance() { + return RefreshAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, RefreshRequest request) { + String index = request.indices() != null ? "/" + String.join(",", request.indices()) : ""; + return newPostRequest(url, index + "/_refresh"); + } + + @Override + protected CheckedFunction entityParser() { + return parser -> new RefreshResponse(); + } +} diff --git a/http/src/main/java/org/elasticsearch/action/admin/indices/settings/put/HttpUpdateSettingsAction.java b/http/src/main/java/org/elasticsearch/action/admin/indices/settings/put/HttpUpdateSettingsAction.java new file mode 100644 index 0000000..8d82bc9 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/admin/indices/settings/put/HttpUpdateSettingsAction.java @@ -0,0 +1,43 @@ +package org.elasticsearch.action.admin.indices.settings.put; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * + */ +public class HttpUpdateSettingsAction extends HttpAction { + + @Override + public UpdateSettingsAction getActionInstance() { + return UpdateSettingsAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, UpdateSettingsRequest request) { + try { + XContentBuilder builder = jsonBuilder(); + builder.startObject(); + request.settings().toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + String index = request.indices() != null ? "/" + String.join(",", request.indices()) : ""; + return newPutRequest(url, index + "/_settings", builder.bytes()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + protected CheckedFunction entityParser() { + return parser -> new UpdateSettingsResponse(); + } +} diff --git a/http/src/main/java/org/elasticsearch/action/bulk/HttpBulkAction.java b/http/src/main/java/org/elasticsearch/action/bulk/HttpBulkAction.java new file mode 100644 index 0000000..050d608 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/bulk/HttpBulkAction.java @@ -0,0 +1,69 @@ +package org.elasticsearch.action.bulk; + +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + * + */ +public class HttpBulkAction extends HttpAction { + + @Override + public BulkAction getActionInstance() { + return BulkAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, BulkRequest request) { + StringBuilder bulkContent = new StringBuilder(); + for (DocWriteRequest actionRequest : request.requests()) { + if (actionRequest instanceof IndexRequest) { + IndexRequest indexRequest = (IndexRequest) actionRequest; + bulkContent.append("{\"").append(indexRequest.opType()).append("\":{"); + bulkContent.append("\"_index\":\"").append(indexRequest.index()).append("\""); + bulkContent.append(",\"_type\":\"").append(indexRequest.type()).append("\""); + if (indexRequest.id() != null) { + bulkContent.append(",\"_id\":\"").append(indexRequest.id()).append("\""); + } + if (indexRequest.routing() != null) { + bulkContent.append(",\"_routing\":\"").append(indexRequest.routing()).append("\""); + } + if (indexRequest.parent() != null) { + bulkContent.append(",\"_parent\":\"").append(indexRequest.parent()).append("\""); + } + if (indexRequest.version() > 0) { + bulkContent.append(",\"_version\":\"").append(indexRequest.version()).append("\""); + if (indexRequest.versionType() != null) { + bulkContent.append(",\"_version_type\":\"").append(indexRequest.versionType().name()).append("\""); + } + } + bulkContent.append("}}\n"); + bulkContent.append(indexRequest.source().utf8ToString()); + bulkContent.append("\n"); + } else if (actionRequest instanceof DeleteRequest) { + DeleteRequest deleteRequest = (DeleteRequest) actionRequest; + bulkContent.append("{\"delete\":{"); + bulkContent.append("\"_index\":\"").append(deleteRequest.index()).append("\""); + bulkContent.append(",\"_type\":\"").append(deleteRequest.type()).append("\""); + bulkContent.append(",\"_id\":\"").append(deleteRequest.id()).append("\""); + if (deleteRequest.routing() != null) { + bulkContent.append(",\"_routing\":\"").append(deleteRequest.routing()).append("\""); // _routing + } + bulkContent.append("}}\n"); + } + } + return newPostRequest(url, "/_bulk", bulkContent.toString()); + } + + @Override + protected CheckedFunction entityParser() { + return BulkResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/get/HttpExistsAction.java b/http/src/main/java/org/elasticsearch/action/get/HttpExistsAction.java new file mode 100644 index 0000000..fd2443e --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/get/HttpExistsAction.java @@ -0,0 +1,29 @@ +package org.elasticsearch.action.get; + +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + */ +public class HttpExistsAction extends HttpAction { + + @Override + public GenericAction getActionInstance() { + return GetAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, GetRequest request) { + return newHeadRequest(url, request.index() + "/" + request.type() + "/" + request.id()); + } + + @Override + protected CheckedFunction entityParser() { + return GetResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/get/HttpGetAction.java b/http/src/main/java/org/elasticsearch/action/get/HttpGetAction.java new file mode 100644 index 0000000..3a72116 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/get/HttpGetAction.java @@ -0,0 +1,29 @@ +package org.elasticsearch.action.get; + +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + */ +public class HttpGetAction extends HttpAction { + + @Override + public GenericAction getActionInstance() { + return GetAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, GetRequest request) { + return newGetRequest(url, request.index() + "/" + request.type() + "/" + request.id()); + } + + @Override + protected CheckedFunction entityParser() { + return GetResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/index/HttpIndexAction.java b/http/src/main/java/org/elasticsearch/action/index/HttpIndexAction.java new file mode 100644 index 0000000..5352682 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/index/HttpIndexAction.java @@ -0,0 +1,30 @@ +package org.elasticsearch.action.index; + +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + */ +public class HttpIndexAction extends HttpAction { + + @Override + public GenericAction getActionInstance() { + return IndexAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, IndexRequest request) { + return newPutRequest(url, request.index() + "/" + request.type() + "/" + request.id(), + request.source()); + } + + @Override + protected CheckedFunction entityParser() { + return IndexResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/main/HttpMainAction.java b/http/src/main/java/org/elasticsearch/action/main/HttpMainAction.java new file mode 100644 index 0000000..ee5dc8c --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/main/HttpMainAction.java @@ -0,0 +1,29 @@ +package org.elasticsearch.action.main; + +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + */ +public class HttpMainAction extends HttpAction { + + @Override + public GenericAction getActionInstance() { + return MainAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, MainRequest request) { + return newGetRequest(url, "/"); + } + + @Override + protected CheckedFunction entityParser() { + return MainResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/search/HttpSearchAction.java b/http/src/main/java/org/elasticsearch/action/search/HttpSearchAction.java new file mode 100644 index 0000000..4c637b7 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/search/HttpSearchAction.java @@ -0,0 +1,33 @@ +package org.elasticsearch.action.search; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + * + */ +public class HttpSearchAction extends HttpAction { + + @Override + public SearchAction getActionInstance() { + return SearchAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, SearchRequest request) { + String index = request.indices() != null ? "/" + String.join(",", request.indices()) : ""; + return newPostRequest(url, index + "/_search", request.source().toString() ); + } + + @Override + protected CheckedFunction entityParser() { + return parser -> { + // TODO(jprante) build search response + return new SearchResponse(); + }; + } +} diff --git a/http/src/main/java/org/elasticsearch/action/update/HttpUpdateAction.java b/http/src/main/java/org/elasticsearch/action/update/HttpUpdateAction.java new file mode 100644 index 0000000..c703075 --- /dev/null +++ b/http/src/main/java/org/elasticsearch/action/update/HttpUpdateAction.java @@ -0,0 +1,61 @@ +package org.elasticsearch.action.update; + +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.client.Requests; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.xbib.elasticsearch.client.http.HttpAction; +import org.xbib.netty.http.client.RequestBuilder; + +import java.io.IOException; + +/** + */ +public class HttpUpdateAction extends HttpAction { + + @Override + public GenericAction getActionInstance() { + return UpdateAction.INSTANCE; + } + + @Override + protected RequestBuilder createHttpRequest(String url, UpdateRequest updateRequest) { + try { + // The Java API allows update requests with different content types + // set for the partial document and the upsert document. This client + // only accepts update requests that have the same content types set + // for both doc and upsert. + XContentType xContentType = null; + if (updateRequest.doc() != null) { + xContentType = updateRequest.doc().getContentType(); + } + if (updateRequest.upsertRequest() != null) { + XContentType upsertContentType = updateRequest.upsertRequest().getContentType(); + if ((xContentType != null) && (xContentType != upsertContentType)) { + throw new IllegalStateException("update request cannot have different content types for doc [" + xContentType + "]" + + " and upsert [" + upsertContentType + "] documents"); + } else { + xContentType = upsertContentType; + } + } + if (xContentType == null) { + xContentType = Requests.INDEX_CONTENT_TYPE; + } + BytesReference source = XContentHelper.toXContent(updateRequest, xContentType, false); + return newPostRequest(url, + updateRequest.index() + "/" + updateRequest.type() + "/" + updateRequest.id() + "/_update", + source); + } catch (IOException e) { + logger.error(e.getMessage(), e); + return null; + } + } + + @Override + protected CheckedFunction entityParser() { + return UpdateResponse::fromXContent; + } +} diff --git a/http/src/main/java/org/xbib/elasticsearch/client/http/HttpAction.java b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpAction.java new file mode 100644 index 0000000..6112f8d --- /dev/null +++ b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpAction.java @@ -0,0 +1,158 @@ +package org.xbib.elasticsearch.client.http; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.RequestBuilder; +import org.xbib.netty.http.client.transport.Transport; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Base class for HTTP actions. + * + * @param the request type + * @param the response type + */ +public abstract class HttpAction { + + protected final Logger logger = LogManager.getLogger(getClass().getName()); + + protected static final String APPLICATION_JSON = "application/json"; + + protected Settings settings; + + protected void setSettings(Settings settings) { + this.settings = settings; + } + + public abstract GenericAction getActionInstance(); + + /*public final ActionFuture execute(HttpActionContext httpActionContext) { + PlainActionFuture future = PlainActionFuture.newFuture(); + //HttpActionFuture future = new HttpActionFuture<>(); + execute(httpActionContext, future); + return future; + }*/ + + public final void execute(HttpActionContext httpActionContext, ActionListener listener) throws IOException { + try { + ActionRequestValidationException validationException = httpActionContext.getRequest().validate(); + if (validationException != null) { + listener.onFailure(validationException); + return; + } + RequestBuilder httpRequestBuilder = + createHttpRequest(httpActionContext.getUrl(), httpActionContext.getRequest()); + //httpRequestBuilder.addHeader("content-type", "application/json"); + Request httpRequest = httpRequestBuilder.build(); +// logger.info("action = {} request = {}", this.getClass().getName(), httpRequest.toString()); + httpRequest.setResponseListener(fullHttpResponse -> { + logger.info("returned response " + fullHttpResponse.status().code() + + " headers = " + fullHttpResponse.headers().entries() + + " content = " + fullHttpResponse.content().toString(StandardCharsets.UTF_8)); + listener.onResponse(parseToResponse(httpActionContext.setHttpResponse(fullHttpResponse))); + }); + Transport transport = httpActionContext.getHttpClient().internalClient().execute(httpRequest); + logger.info("transport = " + transport); + httpActionContext.setHttpClientTransport(transport); + if (transport.isFailed()) { + listener.onFailure(new Exception(transport.getFailure())); + } + logger.info("done, listener is " + listener); + } catch (Throwable e) { + listener.onFailure(new RuntimeException(e)); + throw new IOException(e); + } + } + + protected RequestBuilder newGetRequest(String url, String path) { + return Request.builder(HttpMethod.GET).url(url).uri(path); + } + + protected RequestBuilder newGetRequest(String url, String path, BytesReference content) { + return newRequest(HttpMethod.GET, url, path, content); + } + + protected RequestBuilder newHeadRequest(String url, String path) { + return newRequest(HttpMethod.HEAD, url, path); + } + + protected RequestBuilder newPostRequest(String url, String path) { + return newRequest(HttpMethod.POST, url, path); + } + + protected RequestBuilder newPostRequest(String url, String path, BytesReference content) { + return newRequest(HttpMethod.POST, url, path, content); + } + + protected RequestBuilder newPostRequest(String url, String path, String content) { + return newRequest(HttpMethod.POST, url, path, content); + } + + protected RequestBuilder newPutRequest(String url, String path) { + return newRequest(HttpMethod.PUT, url, path); + } + + protected RequestBuilder newPutRequest(String url, String path, String content) { + return newRequest(HttpMethod.PUT, url, path, content); + } + + protected RequestBuilder newPutRequest(String url, String path, BytesReference content) { + return newRequest(HttpMethod.PUT, url, path, content); + } + + protected RequestBuilder newDeleteRequest(String url, String path, BytesReference content) { + return newRequest(HttpMethod.DELETE, url, path, content); + } + + protected RequestBuilder newRequest(HttpMethod method, String baseUrl, String path) { + return Request.builder(method).url(baseUrl).uri(path); + } + + protected RequestBuilder newRequest(HttpMethod method, String baseUrl, String path, BytesReference content) { + return Request.builder(method).url(baseUrl).uri(path).content(content.toBytesRef().bytes, APPLICATION_JSON); + } + + protected RequestBuilder newRequest(HttpMethod method, String baseUrl, String path, String content) { + return Request.builder(method).url(baseUrl).uri(path).content(content, APPLICATION_JSON); + } + + protected RequestBuilder newRequest(HttpMethod method, String baseUrl, String path, ByteBuf byteBuf) { + return Request.builder(method).url(baseUrl).uri(path).content(byteBuf, APPLICATION_JSON); + } + + protected T parseToResponse(HttpActionContext httpActionContext) { + String mediaType = httpActionContext.getHttpResponse().headers().get(HttpHeaderNames.CONTENT_TYPE); + XContentType xContentType = XContentType.fromMediaTypeOrFormat(mediaType); + if (xContentType == null) { + throw new IllegalStateException("unsupported content-type: " + mediaType); + } + try (XContentParser parser = xContentType.xContent().createParser(httpActionContext.getHttpClient().getRegistry(), + httpActionContext.getHttpResponse().content().array())) { + return entityParser().apply(parser); + } catch (IOException e) { + logger.error(e.getMessage(), e); + return null; + } + } + + protected abstract RequestBuilder createHttpRequest(String baseUrl, R request) throws IOException; + + protected abstract CheckedFunction entityParser(); + +} diff --git a/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionContext.java b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionContext.java new file mode 100644 index 0000000..3d403db --- /dev/null +++ b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionContext.java @@ -0,0 +1,60 @@ +package org.xbib.elasticsearch.client.http; + +import io.netty.handler.codec.http.FullHttpResponse; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.xbib.netty.http.client.transport.Transport; + +/** + * HTTP action context. + * + * @param request type + * @param response type + */ +public class HttpActionContext { + + private final HttpClient httpClient; + + private final R request; + + private final String url; + + private Transport httpClientTransport; + + private FullHttpResponse httpResponse; + + HttpActionContext(HttpClient httpClient, R request, String url) { + this.httpClient = httpClient; + this.request = request; + this.url = url; + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public R getRequest() { + return request; + } + + public String getUrl() { + return url; + } + + public void setHttpClientTransport(Transport httpClientTransport) { + this.httpClientTransport = httpClientTransport; + } + + public Transport getHttpClientTransport() { + return httpClientTransport; + } + + public HttpActionContext setHttpResponse(FullHttpResponse fullHttpResponse) { + this.httpResponse = fullHttpResponse; + return this; + } + + public FullHttpResponse getHttpResponse() { + return httpResponse; + } +} diff --git a/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionFuture.java b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionFuture.java new file mode 100644 index 0000000..33588c8 --- /dev/null +++ b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpActionFuture.java @@ -0,0 +1,100 @@ +package org.xbib.elasticsearch.client.http; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.BaseFuture; +import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException; +import org.xbib.netty.http.client.transport.Transport; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + */ +public class HttpActionFuture extends BaseFuture implements ActionFuture, ActionListener { + + private Transport httpClientTransport; + + HttpActionFuture setHttpClientTransport(Transport httpClientTransport) { + this.httpClientTransport = httpClientTransport; + return this; + } + + @Override + public T actionGet() { + try { + httpClientTransport.get(); + return get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("future got interrupted", e); + } catch (ExecutionException e) { + throw rethrowExecutionException(e); + } + } + + @Override + public T actionGet(String timeout) { + return actionGet(TimeValue.parseTimeValue(timeout, null, getClass().getSimpleName() + ".actionGet.timeout")); + } + + @Override + public T actionGet(long timeoutMillis) { + return actionGet(timeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override + public T actionGet(TimeValue timeout) { + return actionGet(timeout.millis(), TimeUnit.MILLISECONDS); + } + + @Override + public T actionGet(long timeout, TimeUnit unit) { + try { + return get(timeout, unit); + } catch (TimeoutException e) { + throw new ElasticsearchTimeoutException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Future got interrupted", e); + } catch (ExecutionException e) { + throw rethrowExecutionException(e); + } + } + + private static RuntimeException rethrowExecutionException(ExecutionException e) { + if (e.getCause() instanceof ElasticsearchException) { + ElasticsearchException esEx = (ElasticsearchException) e.getCause(); + Throwable root = esEx.unwrapCause(); + if (root instanceof ElasticsearchException) { + return (ElasticsearchException) root; + } else if (root instanceof RuntimeException) { + return (RuntimeException) root; + } + return new UncategorizedExecutionException("Failed execution", root); + } else if (e.getCause() instanceof RuntimeException) { + return (RuntimeException) e.getCause(); + } else { + return new UncategorizedExecutionException("Failed execution", e); + } + } + + @Override + public void onResponse(L result) { + set(convert(result)); + } + + @Override + public void onFailure(Exception e) { + setException(e); + } + + @SuppressWarnings("unchecked") + private T convert(L listenerResponse) { + return (T) listenerResponse; + } +} diff --git a/http/src/main/java/org/xbib/elasticsearch/client/http/HttpClient.java b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpClient.java new file mode 100644 index 0000000..31e5d95 --- /dev/null +++ b/http/src/main/java/org/xbib/elasticsearch/client/http/HttpClient.java @@ -0,0 +1,209 @@ +package org.xbib.elasticsearch.client.http; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.GenericAction; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.node.Node; +import org.elasticsearch.threadpool.ThreadPool; +import org.xbib.elasticsearch.client.AbstractClient; +import org.xbib.elasticsearch.client.BulkControl; +import org.xbib.elasticsearch.client.BulkMetric; +import org.xbib.netty.http.client.Client; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Elasticsearch HTTP client. + */ +public class HttpClient extends AbstractClient implements ElasticsearchClient { + + private static final Logger logger = LogManager.getLogger(HttpClient.class); + + private Client client; + + private NamedXContentRegistry registry; + + @SuppressWarnings("rawtypes") + private Map actionMap; + + private List urls; + + //private ThreadPool threadPool; + + @Override + public HttpClient init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control) { + init(client, settings, metric, control, null, Collections.emptyList()); + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control, + ClassLoader classLoader, List namedXContentEntries) { + //super.init(client, settings, metric, control); + this.urls = settings.getAsList("urls"); + if (urls.isEmpty()) { + throw new IllegalArgumentException("no urls given"); + } + this.registry = new NamedXContentRegistry(Stream.of(getNamedXContents().stream(), + namedXContentEntries.stream() + ).flatMap(Function.identity()).collect(Collectors.toList())); + this.actionMap = new HashMap<>(); + ServiceLoader httpActionServiceLoader = ServiceLoader.load(HttpAction.class, + classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader()); + for (HttpAction httpAction : httpActionServiceLoader) { + httpAction.setSettings(settings); + actionMap.put(httpAction.getActionInstance(), httpAction); + } + this.client = Client.builder().enableDebug().build(); + Settings threadPoolsettings = Settings.builder() + .put(settings) + .put(Node.NODE_NAME_SETTING.getKey(), "httpclient") + .build(); + //this.threadPool = threadPool != null ? threadPool : new ThreadPool(threadPoolsettings); + logger.info("HTTP client initialized with {} actions", actionMap.size()); + } + + private static List getNamedXContents() { + return new ArrayList<>(); + } + + public NamedXContentRegistry getRegistry() { + return registry; + } + + public static Builder builder() { + return new Builder(); + } + + public Client internalClient() { + return client; + } + + @Override + public ElasticsearchClient client() { + return this; + } + + @Override + protected ElasticsearchClient createClient(Settings settings) throws IOException { + return this; + } + + @Override + public void shutdown() throws IOException { + client.shutdownGracefully(); + //threadPool.close(); + } + + @Override + public > ActionFuture + execute(Action action, Request request) { + PlainActionFuture actionFuture = PlainActionFuture.newFuture(); + logger.info("plain action future = " + actionFuture); + execute(action, request, actionFuture); + return actionFuture; + } + + @Override + public > void + execute(Action action, Request request, ActionListener listener) { + doExecute(action, request, listener); + } + + @Override + public > RequestBuilder + prepareExecute(Action action) { + return action.newRequestBuilder(this); + } + + @Override + public ThreadPool threadPool() { + logger.info("returning null for threadPool() request"); + return null; //threadPool; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public > + void doExecute(Action action, R request, ActionListener listener) { + HttpAction httpAction = actionMap.get(action); + if (httpAction == null) { + throw new IllegalStateException("failed to find http action [" + action + "] to execute"); + } + logger.info("http action = " + httpAction); + String url = urls.get(0); // TODO + try { + logger.info("submitting to URL {}", url); + HttpActionContext httpActionContext = new HttpActionContext(this, request, url); + httpAction.execute(httpActionContext, listener); + logger.info("submitted to URL {}", url); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + /** + * The Builder for HTTP client. + */ + public static class Builder { + + private final Settings.Builder settingsBuilder = Settings.builder(); + + private ClassLoader classLoader; + + private List namedXContentEntries; + + private ThreadPool threadPool = null; + + public Builder settings(Settings settings) { + this.settingsBuilder.put(settings); + return this; + } + + public Builder classLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + return this; + } + + public Builder namedXContentEntries(List namedXContentEntries) { + this.namedXContentEntries = namedXContentEntries; + return this; + } + + public Builder threadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public HttpClient build() { + Settings settings = settingsBuilder.build(); + HttpClient httpClient = new HttpClient(); + httpClient.init(null, settings, null, null, + classLoader, namedXContentEntries); + return httpClient; + } + } +} diff --git a/http/src/main/java/org/xbib/elasticsearch/client/http/package-info.java b/http/src/main/java/org/xbib/elasticsearch/client/http/package-info.java new file mode 100644 index 0000000..a9c3ded --- /dev/null +++ b/http/src/main/java/org/xbib/elasticsearch/client/http/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Elasticsearch HTTP client. + */ +package org.xbib.elasticsearch.client.http; diff --git a/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods b/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods new file mode 100644 index 0000000..18f7ab4 --- /dev/null +++ b/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods @@ -0,0 +1 @@ +org.xbib.elasticsearch.client.http.HttpClient \ No newline at end of file diff --git a/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.http.HttpAction b/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.http.HttpAction new file mode 100644 index 0000000..cce80e6 --- /dev/null +++ b/http/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.http.HttpAction @@ -0,0 +1,11 @@ +org.elasticsearch.action.admin.cluster.node.info.HttpNodesInfoAction +org.elasticsearch.action.admin.cluster.settings.HttpClusterUpdateSettingsAction +org.elasticsearch.action.admin.indices.create.HttpCreateIndexAction +org.elasticsearch.action.admin.indices.refresh.HttpRefreshIndexAction +org.elasticsearch.action.admin.indices.settings.put.HttpUpdateSettingsAction +org.elasticsearch.action.bulk.HttpBulkAction +org.elasticsearch.action.index.HttpIndexAction +org.elasticsearch.action.search.HttpSearchAction +org.elasticsearch.action.main.HttpMainAction +org.elasticsearch.action.get.HttpExistsAction +org.elasticsearch.action.get.HttpGetAction diff --git a/http/src/main/resources/extra-security.policy b/http/src/main/resources/extra-security.policy new file mode 100644 index 0000000..a1e19dd --- /dev/null +++ b/http/src/main/resources/extra-security.policy @@ -0,0 +1,20 @@ + +grant codeBase "${codebase.netty-common}" { + // for reading the system-wide configuration for the backlog of established sockets + permission java.io.FilePermission "/proc/sys/net/core/somaxconn", "read"; + // netty makes and accepts socket connections + permission java.net.SocketPermission "*", "accept,connect,resolve"; + // 4.1.24 io.netty.util.concurrent.GlobalEventExecutor$2.run(GlobalEventExecutor.java:228) + permission java.lang.RuntimePermission "setContextClassLoader"; +}; + +grant codeBase "${codebase.netty-transport}" { + // Netty NioEventLoop wants to change this, because of https://bugs.openjdk.java.net/browse/JDK-6427854 + // the bug says it only happened rarely, and that its fixed, but apparently it still happens rarely! + permission java.util.PropertyPermission "sun.nio.ch.bugLevel", "write"; +}; + +grant codeBase "${codebase.netty-http-client}" { + // org.xbib.netty.http.client.Client.(Client.java:67) + permission java.util.PropertyPermission "io.netty.noUnsafe", "write"; +;} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientAliasTests.java b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientAliasTests.java new file mode 100644 index 0000000..09bff1b --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientAliasTests.java @@ -0,0 +1,109 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.IndexAliasAdder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class HttpClientAliasTests extends ESSingleNodeTestCase { + + private static final Logger logger = LogManager.getLogger(HttpClientAliasTests.class.getName()); + + private TransportAddress httpAddress; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + httpAddress = nodeInfo.getHttp().getAddress().publishAddress(); + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_DEFAULT_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + private String findHttpAddress() { + return "http://" + httpAddress.address().getHostName() + ":" + httpAddress.address().getPort(); + } + + public void testIndexAlias() throws Exception { + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + try { + client.newIndex("test1234"); + for (int i = 0; i < 1; i++) { + client.index("test1234", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.refreshIndex("test1234"); + + List simpleAliases = Arrays.asList("a", "b", "c"); + client.switchAliases("test", "test1234", simpleAliases); + + client.newIndex("test5678"); + for (int i = 0; i < 1; i++) { + client.index("test5678", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.refreshIndex("test5678"); + + simpleAliases = Arrays.asList("d", "e", "f"); + client.switchAliases("test", "test5678", simpleAliases, new IndexAliasAdder() { + @Override + public void addIndexAlias(IndicesAliasesRequestBuilder builder, String index, String alias) { + builder.addAlias(index, alias, QueryBuilders.termQuery("my_key", alias)); + } + }); + Map aliases = client.getIndexFilters("test5678"); + logger.info("aliases of index test5678 = {}", aliases); + + aliases = client.getAliasFilters("test"); + logger.info("aliases of alias test = {}", aliases); + + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + client.waitForResponses(TimeValue.timeValueSeconds(30)); + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + } + } +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientDuplicateIDTests.java b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientDuplicateIDTests.java new file mode 100644 index 0000000..456926a --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientDuplicateIDTests.java @@ -0,0 +1,101 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Collection; +import java.util.Collections; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class HttpClientDuplicateIDTests extends ESSingleNodeTestCase { + + private static final Logger logger = LogManager.getLogger(HttpClientDuplicateIDTests.class.getName()); + + private static final long MAX_ACTIONS = 10L; + + private static final long NUM_ACTIONS = 12345L; + + private TransportAddress httpAddress; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + httpAddress = nodeInfo.getHttp().getAddress().publishAddress(); + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_DEFAULT_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + private String findHttpAddress() { + return "http://" + httpAddress.address().getHostName() + ":" + httpAddress.address().getPort(); + } + + public void testDuplicateDocIDs() throws Exception { + final HttpClient client = ClientBuilder.builder() + //.put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException + .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + try { + client.newIndex("test"); + for (int i = 0; i < NUM_ACTIONS; i++) { + client.index("test", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + client.refreshIndex("test"); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) + .setIndices("test") + .setTypes("test") + .setQuery(QueryBuilders.matchAllQuery()); + long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); + logger.info("hits = {}", hits); + assertTrue(hits < NUM_ACTIONS); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + logger.info("numactions = {}, submitted = {}, succeeded= {}, failed = {}", NUM_ACTIONS, + client.getMetric().getSubmitted().getCount(), + client.getMetric().getSucceeded().getCount(), + client.getMetric().getFailed().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSubmitted().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSucceeded().getCount()); + } + } +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientReplicaTests.java b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientReplicaTests.java new file mode 100644 index 0000000..fc036af --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientReplicaTests.java @@ -0,0 +1,142 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.indices.stats.CommonStats; +import org.elasticsearch.action.admin.indices.stats.IndexShardStats; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequestBuilder; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.shard.IndexingStats; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class HttpClientReplicaTests extends ESIntegTestCase { + + private static final Logger logger = LogManager.getLogger(HttpClientReplicaTests.class.getName()); + + private TransportAddress httpAddress; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + httpAddress = nodeInfo.getHttp().getAddress().publishAddress(); + } + + @Override + protected Collection> nodePlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings(int nodeNumber) { + return Settings.builder() + .put(super.nodeSettings(nodeNumber)) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_DEFAULT_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + private String findHttpAddress() { + return "http://" + httpAddress.address().getHostName() + ":" + httpAddress.address().getPort(); + } + + public void testReplicaLevel() throws Exception { + + Settings settingsTest1 = Settings.builder() + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 2) + .build(); + + Settings settingsTest2 = Settings.builder() + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 1) + .build(); + + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + + try { + client.newIndex("test1", settingsTest1, null) + .newIndex("test2", settingsTest2, null); + client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); + for (int i = 0; i < 1234; i++) { + client.index("test1", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + for (int i = 0; i < 1234; i++) { + client.index("test2", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(60)); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + logger.info("refreshing"); + client.refreshIndex("test1"); + client.refreshIndex("test2"); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) + .setIndices("test1", "test2") + .setQuery(QueryBuilders.matchAllQuery()); + long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); + logger.info("query total hits={}", hits); + assertEquals(2468, hits); + IndicesStatsRequestBuilder indicesStatsRequestBuilder = new IndicesStatsRequestBuilder(client.client(), + IndicesStatsAction.INSTANCE) + .all(); + IndicesStatsResponse response = indicesStatsRequestBuilder.execute().actionGet(); + for (Map.Entry m : response.getIndices().entrySet()) { + IndexStats indexStats = m.getValue(); + CommonStats commonStats = indexStats.getTotal(); + IndexingStats indexingStats = commonStats.getIndexing(); + IndexingStats.Stats stats = indexingStats.getTotal(); + logger.info("index {}: count = {}", m.getKey(), stats.getIndexCount()); + for (Map.Entry me : indexStats.getIndexShards().entrySet()) { + IndexShardStats indexShardStats = me.getValue(); + CommonStats commonShardStats = indexShardStats.getTotal(); + logger.info("shard {} count = {}", me.getKey(), + commonShardStats.getIndexing().getTotal().getIndexCount()); + } + } + try { + client.deleteIndex("test1") + .deleteIndex("test2"); + } catch (Exception e) { + logger.error("delete index failed, ignored. Reason:", e); + } + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + } + } + +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientTests.java b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientTests.java new file mode 100644 index 0000000..f64999d --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientTests.java @@ -0,0 +1,204 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Collection; +import java.util.Collections; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class HttpClientTests extends ESSingleNodeTestCase { + + private static final Logger logger = LogManager.getLogger(HttpClientTests.class.getName()); + + private static final Long MAX_ACTIONS = 10L; + + private static final Long NUM_ACTIONS = 1234L; + + private TransportAddress httpAddress; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + httpAddress = nodeInfo.getHttp().getAddress().publishAddress(); + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_DEFAULT_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + private String findHttpAddress() { + return "http://" + httpAddress.address().getHostName() + ":" + httpAddress.address().getPort(); + } + + public void testNewIndex() throws Exception { + final HttpClient client = ClientBuilder.builder() + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + client.newIndex("test"); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + client.shutdown(); + } + + /*public void testMapping() throws Exception { + final HttpClient client = ClientBuilder.builder() + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + XContentBuilder builder = XContentFactory.jsonBuilder() + .startObject() + .startObject("test") + .startObject("properties") + .startObject("location") + .field("type", "geo_point") + .endObject() + .endObject() + .endObject() + .endObject(); + client.mapping("test", builder.string()); + client.newIndex("test"); + GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices("test"); + GetMappingsResponse getMappingsResponse = + client.client().execute(GetMappingsAction.INSTANCE, getMappingsRequest).actionGet(); + logger.info("mappings={}", getMappingsResponse.getMappings()); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + client.shutdown(); + } + + public void testSingleDoc() throws Exception { + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(30)) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + client.newIndex("test"); + client.index("test", "test", "1", false,"{ \"name\" : \"Hello World\"}"); // single doc ingest + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + assertEquals(1, client.getMetric().getSucceeded().getCount()); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + client.shutdown(); + } + + public void testRandomDocs() throws Exception { + long numactions = NUM_ACTIONS; + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + try { + client.newIndex("test"); + for (int i = 0; i < NUM_ACTIONS; i++) { + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + logger.info("assuring {} == {}", numactions, client.getMetric().getSucceeded().getCount()); + assertEquals(numactions, client.getMetric().getSucceeded().getCount()); + assertFalse(client.hasThrowable()); + client.shutdown(); + } + } + + public void testThreadedRandomDocs() throws Exception { + int maxthreads = Runtime.getRuntime().availableProcessors(); + Long maxactions = MAX_ACTIONS; + final Long maxloop = NUM_ACTIONS; + logger.info("max={} maxactions={} maxloop={}", maxthreads, maxactions, maxloop); + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, maxactions) + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60))// disable auto flush for this test + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + try { + client.newIndex("test").startBulk("test", 30 * 1000, 1000); + ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); + final CountDownLatch latch = new CountDownLatch(maxthreads); + for (int i = 0; i < maxthreads; i++) { + executorService.execute(() -> { + for (int i1 = 0; i1 < maxloop; i1++) { + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + latch.countDown(); + }); + } + logger.info("waiting for max 30 seconds..."); + latch.await(30, TimeUnit.SECONDS); + logger.info("flush..."); + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + logger.info("got all responses, executor service shutdown..."); + executorService.shutdown(); + logger.info("executor service is shut down"); + client.stopBulk("test"); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + logger.info("assuring {} == {}", maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); + assertEquals(maxthreads * maxloop, client.getMetric().getSucceeded().getCount()); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + client.refreshIndex("test"); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) + .setIndices("test") + .setQuery(QueryBuilders.matchAllQuery()) + .setSize(0); + assertEquals(maxthreads * maxloop, + searchRequestBuilder.execute().actionGet().getHits().getTotalHits()); + client.shutdown(); + } + }*/ +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientUpdateReplicaLevelTests.java b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientUpdateReplicaLevelTests.java new file mode 100644 index 0000000..db0f894 --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/HttpClientUpdateReplicaLevelTests.java @@ -0,0 +1,97 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Collection; +import java.util.Collections; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class HttpClientUpdateReplicaLevelTests extends ESIntegTestCase { + + private static final Logger logger = LogManager.getLogger(HttpClientUpdateReplicaLevelTests.class.getName()); + + private TransportAddress httpAddress; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + httpAddress = nodeInfo.getHttp().getAddress().publishAddress(); + } + + @Override + protected Collection> nodePlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings(int nodeNumber) { + return Settings.builder() + .put(super.nodeSettings(nodeNumber)) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_DEFAULT_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.HTTP_ENABLED.getKey(), true) + .build(); + } + + private String findHttpAddress() { + return "http://" + httpAddress.address().getHostName() + ":" + httpAddress.address().getPort(); + } + + public void testUpdateReplicaLevel() throws Exception { + + int numberOfShards = 1; + int replicaLevel = 2; + + int shardsAfterReplica; + + Settings settings = Settings.builder() + .put("index.number_of_shards", numberOfShards) + .put("index.number_of_replicas", 0) + .build(); + + final HttpClient client = ClientBuilder.builder() + .put("urls", findHttpAddress()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(client(), HttpClient.class); + + try { + client.newIndex("replicatest", settings, null); + client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); + for (int i = 0; i < 12345; i++) { + client.index("replicatest", "replicatest", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + shardsAfterReplica = client.updateReplicaLevel("replicatest", replicaLevel); + assertEquals(shardsAfterReplica, numberOfShards * (replicaLevel + 1)); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + } + } + +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/IndexCreationTest.java b/http/src/test/java/org/xbib/elasticsearch/client/http/IndexCreationTest.java new file mode 100644 index 0000000..db768be --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/IndexCreationTest.java @@ -0,0 +1,50 @@ +package org.xbib.elasticsearch.client.http; + +import org.junit.Test; +import org.xbib.elasticsearch.client.ClientBuilder; + +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public class IndexCreationTest { + + private static final Logger logger = Logger.getLogger(IndexCreationTest.class.getName()); + static { + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + System.setProperty("log4j2.disable.jmx", Boolean.toString(true)); + + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.ALL); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } + + @Test + public void testNewIndex() throws Exception { + HttpClient client = ClientBuilder.builder() + //.put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) + .put("urls", "http://localhost:9200") + //.setMetric(new SimpleBulkMetric()) + //.setControl(new SimpleBulkControl()) + .getClient(HttpClient.class); + try { + client.newIndex("demo"); + Thread.sleep(3000L); + } finally { + client.shutdown(); + } + } +} diff --git a/http/src/test/java/org/xbib/elasticsearch/client/http/TestRunnerThreadsFilter.java b/http/src/test/java/org/xbib/elasticsearch/client/http/TestRunnerThreadsFilter.java new file mode 100644 index 0000000..15e845e --- /dev/null +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/TestRunnerThreadsFilter.java @@ -0,0 +1,11 @@ +package org.xbib.elasticsearch.client.http; + +import com.carrotsearch.randomizedtesting.ThreadFilter; + +public class TestRunnerThreadsFilter implements ThreadFilter { + + @Override + public boolean reject(Thread thread) { + return thread.getName().startsWith("ObjectCleanerThread"); + } +} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/package-info.java b/http/src/test/java/org/xbib/elasticsearch/client/http/package-info.java similarity index 56% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/package-info.java rename to http/src/test/java/org/xbib/elasticsearch/client/http/package-info.java index 873ebae..aea79de 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/package-info.java +++ b/http/src/test/java/org/xbib/elasticsearch/client/http/package-info.java @@ -1,4 +1,4 @@ /** * Classes for testing Elasticsearch node client extras. */ -package org.xbib.elasticsearch.extras.client.node; +package org.xbib.elasticsearch.client.http; diff --git a/node/build.gradle b/node/build.gradle new file mode 100644 index 0000000..1c7e38b --- /dev/null +++ b/node/build.gradle @@ -0,0 +1,63 @@ +buildscript { + repositories { + jcenter() + maven { + url 'http://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.2.2.0" + } +} + +apply plugin: 'org.xbib.gradle.plugin.elasticsearch.build' + +configurations { + main + tests +} + +dependencies { + compile project(':common') + testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" + testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" +} + +jar { + baseName "${rootProject.name}-node" +} + +task testJar(type: Jar, dependsOn: testClasses) { + baseName = "${project.archivesBaseName}-tests" + from sourceSets.test.output +} + +artifacts { + main jar + tests testJar + archives sourcesJar, javadocJar +} + +test { + enabled = false + jvmArgs "-javaagent:" + configurations.alpnagent.asPath + systemProperty 'path.home', projectDir.absolutePath + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} + +randomizedTest { + enabled = false +} + + +esTest { + // test with the jars, not the classes, for security manager + classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files + systemProperty 'tests.security.manager', 'true' + // maybe we like some extra security policy for our code + systemProperty 'tests.security.policy', '/extra-security.policy' +} +esTest.dependsOn jar, testJar diff --git a/node/config/checkstyle/checkstyle.xml b/node/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/node/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/node/src/docs/asciidoc/css/foundation.css b/node/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/node/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/node/src/docs/asciidoclet/overview.adoc b/node/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..7947331 --- /dev/null +++ b/node/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Elasticsearch Java client +Jörg Prante +Version 5.4.0.0 + diff --git a/node/src/main/java/org/xbib/elasticsearch/client/node/NodeBulkClient.java b/node/src/main/java/org/xbib/elasticsearch/client/node/NodeBulkClient.java new file mode 100644 index 0000000..103c1be --- /dev/null +++ b/node/src/main/java/org/xbib/elasticsearch/client/node/NodeBulkClient.java @@ -0,0 +1,79 @@ +package org.xbib.elasticsearch.client.node; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.node.Node; +import org.elasticsearch.node.NodeValidationException; +import org.elasticsearch.plugins.Plugin; +import org.xbib.elasticsearch.client.AbstractClient; +import org.xbib.elasticsearch.client.BulkControl; +import org.xbib.elasticsearch.client.BulkMetric; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +/** + * + */ +public class NodeBulkClient extends AbstractClient { + + private static final Logger logger = LogManager.getLogger(NodeBulkClient.class.getName()); + + private Node node; + + public NodeBulkClient init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control) { + super.init(client, settings, metric, control); + return this; + } + + @Override + protected ElasticsearchClient createClient(Settings settings) throws IOException { + if (settings != null) { + String version = System.getProperty("os.name") + + " " + System.getProperty("java.vm.name") + + " " + System.getProperty("java.vm.vendor") + + " " + System.getProperty("java.runtime.version") + + " " + System.getProperty("java.vm.version"); + Settings effectiveSettings = Settings.builder().put(settings) + .put("node.client", true) + .put("node.master", false) + .put("node.data", false) + .build(); + logger.info("creating node client on {} with effective settings {}", + version, effectiveSettings.toString()); + Collection> plugins = Collections.emptyList(); + this.node = new BulkNode(new Environment(effectiveSettings, null), plugins); + try { + node.start(); + } catch (NodeValidationException e) { + throw new IOException(e); + } + return node.client(); + } + return null; + } + + @Override + public synchronized void shutdown() throws IOException { + super.shutdown(); + try { + if (node != null) { + logger.debug("closing node..."); + node.close(); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + private static class BulkNode extends Node { + + BulkNode(Environment env, Collection> classpathPlugins) { + super(env, classpathPlugins); + } + } +} diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/node/package-info.java b/node/src/main/java/org/xbib/elasticsearch/client/node/package-info.java similarity index 52% rename from src/main/java/org/xbib/elasticsearch/extras/client/node/package-info.java rename to node/src/main/java/org/xbib/elasticsearch/client/node/package-info.java index c5c0895..08795e8 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/node/package-info.java +++ b/node/src/main/java/org/xbib/elasticsearch/client/node/package-info.java @@ -1,4 +1,4 @@ /** * Classes for Elasticsearch node client extras. */ -package org.xbib.elasticsearch.extras.client.node; +package org.xbib.elasticsearch.client.node; diff --git a/node/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods b/node/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods new file mode 100644 index 0000000..631ddb7 --- /dev/null +++ b/node/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods @@ -0,0 +1 @@ +org.xbib.elasticsearch.client.node.NodeBulkClient \ No newline at end of file diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientDuplicateIDTests.java similarity index 69% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java rename to node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientDuplicateIDTests.java index 95f3ca8..5681d63 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeDuplicateIDTest.java +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientDuplicateIDTests.java @@ -1,46 +1,39 @@ -package org.xbib.elasticsearch.extras.client.node; - -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +package org.xbib.elasticsearch.client.node; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.unit.TimeValue; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; -/** - * - */ -public class BulkNodeDuplicateIDTest extends NodeTestBase { +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; - private static final Logger logger = LogManager.getLogger(BulkNodeDuplicateIDTest.class.getName()); +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class NodeBulkClientDuplicateIDTests extends ESSingleNodeTestCase { + + private static final Logger logger = LogManager.getLogger(NodeBulkClientDuplicateIDTests.class.getName()); private static final long MAX_ACTIONS = 100L; private static final long NUM_ACTIONS = 12345L; - @Test public void testDuplicateDocIDs() throws Exception { - - final BulkNodeClient client = ClientBuilder.builder() - .put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException + final NodeBulkClient client = ClientBuilder.builder() + //.put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("test"); for (int i = 0; i < NUM_ACTIONS; i++) { - client.index("test", "test", randomString(1), "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientIndexAliasTests.java similarity index 67% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java rename to node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientIndexAliasTests.java index 1c3b3fd..7f36794 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeIndexAliasTest.java +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientIndexAliasTests.java @@ -1,41 +1,36 @@ -package org.xbib.elasticsearch.extras.client.node; - -import static org.junit.Assert.assertFalse; +package org.xbib.elasticsearch.client.node; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilders; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.IndexAliasAdder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.IndexAliasAdder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; import java.util.Arrays; import java.util.List; import java.util.Map; -/** - * - */ -public class BulkNodeIndexAliasTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class NodeBulkClientIndexAliasTests extends ESSingleNodeTestCase { - private static final Logger logger = LogManager.getLogger(BulkNodeIndexAliasTest.class.getName()); + private static final Logger logger = LogManager.getLogger(NodeBulkClientIndexAliasTests.class.getName()); - @Test public void testIndexAlias() throws Exception { - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("test1234"); for (int i = 0; i < 1; i++) { - client.index("test1234", "test", randomString(1), "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test1234", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.refreshIndex("test1234"); @@ -45,7 +40,7 @@ public class BulkNodeIndexAliasTest extends NodeTestBase { client.newIndex("test5678"); for (int i = 0; i < 1; i++) { - client.index("test5678", "test", randomString(1), "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test5678", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.refreshIndex("test5678"); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientReplicaTests.java similarity index 75% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java rename to node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientReplicaTests.java index b0d8100..c8e6186 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeReplicaTest.java +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientReplicaTests.java @@ -1,9 +1,6 @@ -package org.xbib.elasticsearch.extras.client.node; - -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +package org.xbib.elasticsearch.client.node; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.stats.CommonStats; @@ -17,54 +14,47 @@ import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.shard.IndexingStats; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESIntegTestCase; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; import java.util.Map; -/** - * - */ -public class BulkNodeReplicaTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class NodeBulkClientReplicaTests extends ESIntegTestCase { - private static final Logger logger = LogManager.getLogger(BulkNodeReplicaTest.class.getName()); + private static final Logger logger = LogManager.getLogger(NodeBulkClientReplicaTests.class.getName()); - @Test public void testReplicaLevel() throws Exception { - // we need nodes for replica levels - startNode("2"); - startNode("3"); - startNode("4"); - Settings settingsTest1 = Settings.builder() - .put("index.number_of_shards", 2) - .put("index.number_of_replicas", 3) + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 2) .build(); Settings settingsTest2 = Settings.builder() - .put("index.number_of_shards", 2) + .put("index.number_of_shards", 1) .put("index.number_of_replicas", 1) .build(); - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("test1", settingsTest1, null) .newIndex("test2", settingsTest2, null); client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); for (int i = 0; i < 1234; i++) { - client.index("test1", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test1", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } for (int i = 0; i < 1234; i++) { - client.index("test2", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test2", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(60)); @@ -76,7 +66,7 @@ public class BulkNodeReplicaTest extends NodeTestBase { client.refreshIndex("test2"); SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) .setIndices("test1", "test2") - .setQuery(matchAllQuery()); + .setQuery(QueryBuilders.matchAllQuery()); long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); logger.info("query total hits={}", hits); assertEquals(2468, hits); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientTests.java similarity index 70% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java rename to node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientTests.java index 421be5f..6b261df 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClientTest.java +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientTests.java @@ -1,9 +1,6 @@ -package org.xbib.elasticsearch.extras.client.node; - -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +package org.xbib.elasticsearch.client.node; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; @@ -14,48 +11,33 @@ import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.QueryBuilders; -import org.junit.Before; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -/** - * - */ -public class BulkNodeClientTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class NodeBulkClientTests extends ESSingleNodeTestCase { - private static final Logger logger = LogManager.getLogger(BulkNodeClientTest.class.getName()); + private static final Logger logger = LogManager.getLogger(NodeBulkClientTests.class.getName()); - private static final Long MAX_ACTIONS = 1000L; + private static final Long MAX_ACTIONS = 10L; private static final Long NUM_ACTIONS = 1234L; - @Before - public void startNodes() { - try { - super.startNodes(); - startNode("2"); - } catch (Throwable t) { - logger.error("startNodes failed", t); - } - } - - @Test public void testNewIndexNodeClient() throws Exception { - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); client.newIndex("test"); if (client.hasThrowable()) { logger.error("error", client.getThrowable()); @@ -64,14 +46,13 @@ public class BulkNodeClientTest extends NodeTestBase { client.shutdown(); } - @Test public void testBulkNodeClientMapping() throws Exception { - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); - XContentBuilder builder = jsonBuilder() + .getClient(client(), NodeBulkClient.class); + XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .startObject("test") .startObject("properties") @@ -94,48 +75,37 @@ public class BulkNodeClientTest extends NodeTestBase { client.shutdown(); } - @Test - public void testBulkNodeClientSingleDoc() { - final BulkNodeClient client = ClientBuilder.builder() + public void testBulkNodeClientSingleDoc() throws Exception { + final NodeBulkClient client = ClientBuilder.builder() .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(30)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); - try { - client.newIndex("test"); - client.index("test", "test", "1", "{ \"name\" : \"Hello World\"}"); // single doc ingest - client.flushIngest(); - client.waitForResponses(TimeValue.timeValueSeconds(30)); - } catch (InterruptedException e) { - // ignore - } catch (NoNodeAvailableException e) { - logger.warn("skipping, no node available"); - } catch (ExecutionException e) { - logger.error(e.getMessage(), e); - } finally { - assertEquals(1, client.getMetric().getSucceeded().getCount()); - if (client.hasThrowable()) { - logger.error("error", client.getThrowable()); - } - assertFalse(client.hasThrowable()); - client.shutdown(); + .getClient(client(), NodeBulkClient.class); + client.newIndex("test"); + client.index("test", "test", "1", false, "{ \"name\" : \"Hello World\"}"); // single doc ingest + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + assertEquals(1, client.getMetric().getSucceeded().getCount()); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); } + assertFalse(client.hasThrowable()); + client.shutdown(); } - @Test public void testBulkNodeClientRandomDocs() throws Exception { long numactions = NUM_ACTIONS; - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("test"); for (int i = 0; i < NUM_ACTIONS; i++) { - client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); @@ -152,18 +122,17 @@ public class BulkNodeClientTest extends NodeTestBase { } } - @Test public void testBulkNodeClientThreadedRandomDocs() throws Exception { int maxthreads = Runtime.getRuntime().availableProcessors(); Long maxactions = MAX_ACTIONS; final Long maxloop = NUM_ACTIONS; logger.info("NodeClient max={} maxactions={} maxloop={}", maxthreads, maxactions, maxloop); - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, maxactions) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60))// disable auto flush for this test .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("test").startBulk("test", 30 * 1000, 1000); ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); @@ -171,7 +140,7 @@ public class BulkNodeClientTest extends NodeTestBase { for (int i = 0; i < maxthreads; i++) { executorService.execute(() -> { for (int i1 = 0; i1 < maxloop; i1++) { - client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } latch.countDown(); }); diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientUpdateReplicaLevelTests.java similarity index 62% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java rename to node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientUpdateReplicaLevelTests.java index 6d5cbe1..97ab80d 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/node/BulkNodeUpdateReplicaLevelTest.java +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/NodeBulkClientUpdateReplicaLevelTests.java @@ -1,35 +1,26 @@ -package org.xbib.elasticsearch.extras.client.node; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +package org.xbib.elasticsearch.client.node; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESIntegTestCase; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; -/** - * - */ -public class BulkNodeUpdateReplicaLevelTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class NodeBulkClientUpdateReplicaLevelTests extends ESIntegTestCase { - private static final Logger logger = LogManager.getLogger(BulkNodeUpdateReplicaLevelTest.class.getName()); + private static final Logger logger = LogManager.getLogger(NodeBulkClientUpdateReplicaLevelTests.class.getName()); - @Test public void testUpdateReplicaLevel() throws Exception { - int numberOfShards = 2; - int replicaLevel = 3; - - // we need 3 nodes for replica level 3 - startNode("2"); - startNode("3"); + int numberOfShards = 1; + int replicaLevel = 2; int shardsAfterReplica; @@ -38,16 +29,16 @@ public class BulkNodeUpdateReplicaLevelTest extends NodeTestBase { .put("index.number_of_replicas", 0) .build(); - final BulkNodeClient client = ClientBuilder.builder() + final NodeBulkClient client = ClientBuilder.builder() .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkNodeClient(client("1")); + .getClient(client(), NodeBulkClient.class); try { client.newIndex("replicatest", settings, null); client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); for (int i = 0; i < 12345; i++) { - client.index("replicatest", "replicatest", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("replicatest", "replicatest", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); @@ -63,5 +54,4 @@ public class BulkNodeUpdateReplicaLevelTest extends NodeTestBase { assertFalse(client.hasThrowable()); } } - } diff --git a/node/src/test/java/org/xbib/elasticsearch/client/node/TestRunnerThreadsFilter.java b/node/src/test/java/org/xbib/elasticsearch/client/node/TestRunnerThreadsFilter.java new file mode 100644 index 0000000..6d0252d --- /dev/null +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/TestRunnerThreadsFilter.java @@ -0,0 +1,11 @@ +package org.xbib.elasticsearch.client.node; + +import com.carrotsearch.randomizedtesting.ThreadFilter; + +public class TestRunnerThreadsFilter implements ThreadFilter { + + @Override + public boolean reject(Thread thread) { + return thread.getName().startsWith("ObjectCleanerThread"); + } +} diff --git a/node/src/test/java/org/xbib/elasticsearch/client/node/package-info.java b/node/src/test/java/org/xbib/elasticsearch/client/node/package-info.java new file mode 100644 index 0000000..f0ef244 --- /dev/null +++ b/node/src/test/java/org/xbib/elasticsearch/client/node/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing Elasticsearch node client extras. + */ +package org.xbib.elasticsearch.client.node; diff --git a/node/src/test/resources/log4j2.xml b/node/src/test/resources/log4j2.xml new file mode 100644 index 0000000..b175dfc --- /dev/null +++ b/node/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/integration-test/resources/org/xbib/elasticsearch/extras/client/settings.json b/node/src/test/resources/org/xbib/elasticsearch/client/node/settings.json similarity index 100% rename from src/integration-test/resources/org/xbib/elasticsearch/extras/client/settings.json rename to node/src/test/resources/org/xbib/elasticsearch/client/node/settings.json diff --git a/settings.gradle b/settings.gradle index 89f4110..638442b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,6 @@ -rootProject.name = name + +include 'api' +include 'common' +include 'node' +include 'transport' +include 'http' diff --git a/src/integration-test/java/org/elasticsearch/node/MockNode.java b/src/integration-test/java/org/elasticsearch/node/MockNode.java deleted file mode 100644 index 7b5d5b3..0000000 --- a/src/integration-test/java/org/elasticsearch/node/MockNode.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.elasticsearch.node; - -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.plugins.Plugin; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * - */ -public class MockNode extends Node { - - public MockNode() { - super(Settings.EMPTY); - } - - public MockNode(Settings settings) { - super(settings); - } - - public MockNode(Settings settings, Class classpathPlugin) { - this(settings, list(classpathPlugin)); - } - - public MockNode(Settings settings, Collection> classpathPlugins) { - super(InternalSettingsPreparer.prepareEnvironment(settings, null), classpathPlugins); - } - - private static Collection> list(Class classpathPlugin) { - Collection> list = new ArrayList<>(); - list.add(classpathPlugin); - return list; - } - -} diff --git a/src/integration-test/java/org/elasticsearch/node/package-info.java b/src/integration-test/java/org/elasticsearch/node/package-info.java deleted file mode 100644 index f299cbc..0000000 --- a/src/integration-test/java/org/elasticsearch/node/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Classes to support Elasticsearch node creation. - */ -package org.elasticsearch.node; diff --git a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java b/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java deleted file mode 100644 index 618fa06..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/NodeTestBase.java +++ /dev/null @@ -1,233 +0,0 @@ -package org.xbib.elasticsearch; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; -import org.elasticsearch.client.support.AbstractClient; -import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.InetSocketTransportAddress; -import org.elasticsearch.common.transport.LocalTransportAddress; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.node.MockNode; -import org.elasticsearch.node.Node; -import org.elasticsearch.node.NodeValidationException; -import org.elasticsearch.transport.Netty4Plugin; -import org.junit.After; -import org.junit.Before; -import org.xbib.elasticsearch.extras.client.NetworkUtils; - -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * - */ -public class NodeTestBase { - - private static final Logger logger = LogManager.getLogger("test"); - - private static final Random random = new Random(); - - private static final char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz").toCharArray(); - - private Map nodes = new HashMap<>(); - - private Map clients = new HashMap<>(); - - private AtomicInteger counter = new AtomicInteger(); - - private String clustername; - - private String host; - - private int port; - - @Before - public void startNodes() { - try { - logger.info("settings cluster name"); - setClusterName(); - logger.info("starting nodes"); - startNode("1"); - findNodeAddress(); - ClusterHealthResponse healthResponse = client("1").execute(ClusterHealthAction.INSTANCE, - new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.GREEN) - .timeout(TimeValue.timeValueSeconds(30))).actionGet(); - if (healthResponse != null && healthResponse.isTimedOut()) { - throw new IOException("cluster state is " + healthResponse.getStatus().name() - + ", from here on, everything will fail!"); - } - logger.info("nodes are started"); - } catch (Throwable t) { - logger.error("start of nodes failed", t); - } - } - - @After - public void stopNodes() { - try { - logger.info("stopping nodes"); - closeNodes(); - } catch (Throwable e) { - logger.error("can not close nodes", e); - } finally { - try { - deleteFiles(); - logger.info("data files wiped"); - Thread.sleep(2000L); // let OS commit changes - } catch (IOException e) { - logger.error(e.getMessage(), e); - } catch (InterruptedException e) { - // ignore - } - } - } - - protected void setClusterName() { - this.clustername = "test-helper-cluster-" - + NetworkUtils.getLocalAddress().getHostName() - + "-" + System.getProperty("user.name") - + "-" + counter.incrementAndGet(); - } - - protected String getClusterName() { - return clustername; - } - - protected Settings getNodeSettings() { - String hostname = NetworkUtils.getLocalAddress().getHostName(); - return Settings.builder() - .put("cluster.name", clustername) - // required to build a cluster, replica tests will test this. - .put("discovery.zen.ping.unicast.hosts", hostname) - .put("transport.type", Netty4Plugin.NETTY_TRANSPORT_NAME) - .put("network.host", hostname) - .put("http.enabled", false) - .put("path.home", getHome()) - // maximum five nodes on same host - .put("node.max_local_storage_nodes", 5) - .put("thread_pool.bulk.size", Runtime.getRuntime().availableProcessors()) - // default is 50 which is too low - .put("thread_pool.bulk.queue_size", 16 * Runtime.getRuntime().availableProcessors()) - .build(); - } - - - protected Settings getClientSettings() { - if (host == null) { - throw new IllegalStateException("host is null"); - } - // the host to which transport client should connect to - return Settings.builder() - .put("cluster.name", clustername) - .put("host", host + ":" + port) - .build(); - } - - protected String getHome() { - return System.getProperty("path.home"); - } - - public void startNode(String id) throws IOException { - try { - buildNode(id).start(); - } catch (NodeValidationException e) { - throw new IOException(e); - } - } - - public AbstractClient client(String id) { - return clients.get(id); - } - - private void closeNodes() throws IOException { - logger.info("closing all clients"); - for (AbstractClient client : clients.values()) { - client.close(); - } - clients.clear(); - logger.info("closing all nodes"); - for (Node node : nodes.values()) { - if (node != null) { - node.close(); - } - } - nodes.clear(); - logger.info("all nodes closed"); - } - - protected void findNodeAddress() { - NodesInfoRequest nodesInfoRequest = new NodesInfoRequest().transport(true); - NodesInfoResponse response = client("1").admin().cluster().nodesInfo(nodesInfoRequest).actionGet(); - Object obj = response.getNodes().iterator().next().getTransport().getAddress() - .publishAddress(); - if (obj instanceof InetSocketTransportAddress) { - InetSocketTransportAddress address = (InetSocketTransportAddress) obj; - host = address.address().getHostName(); - port = address.address().getPort(); - } else if (obj instanceof LocalTransportAddress) { - LocalTransportAddress address = (LocalTransportAddress) obj; - host = address.getHost(); - port = address.getPort(); - } else { - logger.info("class=" + obj.getClass()); - } - if (host == null) { - throw new IllegalArgumentException("host not found"); - } - } - - private Node buildNode(String id) throws IOException { - Settings nodeSettings = Settings.builder() - .put(getNodeSettings()) - .build(); - logger.info("settings={}", nodeSettings.getAsMap()); - Node node = new MockNode(nodeSettings, Netty4Plugin.class); - AbstractClient client = (AbstractClient) node.client(); - nodes.put(id, node); - clients.put(id, client); - logger.info("clients={}", clients); - return node; - } - - protected String randomString(int len) { - final char[] buf = new char[len]; - final int n = numbersAndLetters.length - 1; - for (int i = 0; i < buf.length; i++) { - buf[i] = numbersAndLetters[random.nextInt(n)]; - } - return new String(buf); - } - - private static void deleteFiles() throws IOException { - Path directory = Paths.get(System.getProperty("path.home") + "/data"); - Files.walkFileTree(directory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - - }); - } -} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java deleted file mode 100644 index adfc417..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/ClusterBlockTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.xbib.elasticsearch.extras.client; - -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.bulk.BulkRequestBuilder; -import org.elasticsearch.action.index.IndexRequestBuilder; -import org.elasticsearch.cluster.block.ClusterBlockException; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentType; -import org.junit.Before; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; - -/** - * - */ -public class ClusterBlockTest extends NodeTestBase { - - private static final Logger logger = LogManager.getLogger(ClusterBlockTest.class.getName()); - - @Before - public void startNodes() { - try { - setClusterName(); - startNode("1"); - findNodeAddress(); - // do not wait for green health state - logger.info("ready"); - } catch (Throwable t) { - logger.error("startNodes failed", t); - } - } - - protected Settings getNodeSettings() { - return Settings.builder() - .put(super.getNodeSettings()) - .put("discovery.zen.minimum_master_nodes", 2) // block until we have two nodes - .build(); - } - - @Test(expected = ClusterBlockException.class) - public void testClusterBlock() throws Exception { - BulkRequestBuilder brb = client("1").prepareBulk(); - XContentBuilder builder = jsonBuilder().startObject().field("field1", "value1").endObject(); - String jsonString = builder.string(); - IndexRequestBuilder irb = client("1").prepareIndex("test", "test", "1") - .setSource(jsonString, XContentType.JSON); - brb.add(irb); - brb.execute().actionGet(); - } - -} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java deleted file mode 100644 index 9b16eda..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/WildcardTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.xbib.elasticsearch.extras.client; - -import static org.elasticsearch.client.Requests.indexRequest; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery; - -import org.elasticsearch.action.support.WriteRequest; -import org.elasticsearch.client.Client; -import org.elasticsearch.index.query.QueryBuilder; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; - -import java.io.IOException; - -/** - * - */ -public class WildcardTest extends NodeTestBase { - - @Test - public void testWildcard() throws Exception { - Client client = client("1"); - index(client, "1", "010"); - index(client, "2", "0*0"); - // exact - validateCount(client, queryStringQuery("010").defaultField("field"), 1); - validateCount(client, queryStringQuery("0\\*0").defaultField("field"), 1); - // pattern - validateCount(client, queryStringQuery("0*0").defaultField("field"), 1); // 2? - validateCount(client, queryStringQuery("0?0").defaultField("field"), 1); // 2? - validateCount(client, queryStringQuery("0**0").defaultField("field"), 1); // 2? - validateCount(client, queryStringQuery("0??0").defaultField("field"), 0); - validateCount(client, queryStringQuery("*10").defaultField("field"), 1); - validateCount(client, queryStringQuery("*1*").defaultField("field"), 1); - validateCount(client, queryStringQuery("*\\*0").defaultField("field"), 0); // 1? - validateCount(client, queryStringQuery("*\\**").defaultField("field"), 0); // 1? - } - - private void index(Client client, String id, String fieldValue) throws IOException { - client.index(indexRequest() - .index("index").type("type").id(id) - .source(jsonBuilder().startObject().field("field", fieldValue).endObject()) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)) - .actionGet(); - } - - private long count(Client client, QueryBuilder queryBuilder) { - return client.prepareSearch("index").setTypes("type") - .setQuery(queryBuilder) - .execute().actionGet().getHits().getTotalHits(); - } - - private void validateCount(Client client, QueryBuilder queryBuilder, long expectedHits) { - final long actualHits = count(client, queryBuilder); - if (actualHits != expectedHits) { - throw new RuntimeException("actualHits=" + actualHits + ", expectedHits=" + expectedHits); - } - } -} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java deleted file mode 100644 index 9783dc2..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportDuplicateIDTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.xbib.elasticsearch.extras.client.transport; - -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.search.SearchAction; -import org.elasticsearch.action.search.SearchRequestBuilder; -import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.unit.TimeValue; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; - -/** - * - */ -public class BulkTransportDuplicateIDTest extends NodeTestBase { - - private static final Logger logger = LogManager.getLogger(BulkTransportDuplicateIDTest.class.getName()); - - private static final long MAX_ACTIONS = 100L; - - private static final long NUM_ACTIONS = 12345L; - - @Test - public void testDuplicateDocIDs() throws Exception { - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) - .put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException - .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) - .setMetric(new SimpleBulkMetric()) - .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); - try { - client.newIndex("test"); - for (int i = 0; i < NUM_ACTIONS; i++) { - client.index("test", "test", randomString(1), "{ \"name\" : \"" + randomString(32) + "\"}"); - } - client.flushIngest(); - client.waitForResponses(TimeValue.timeValueSeconds(30)); - client.refreshIndex("test"); - SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) - .setIndices("test") - .setTypes("test") - .setQuery(matchAllQuery()); - long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); - logger.info("hits = {}", hits); - assertTrue(hits < NUM_ACTIONS); - } catch (NoNodeAvailableException e) { - logger.warn("skipping, no node available"); - } finally { - client.shutdown(); - if (client.hasThrowable()) { - logger.error("error", client.getThrowable()); - } - assertFalse(client.hasThrowable()); - logger.info("numactions = {}, submitted = {}, succeeded= {}, failed = {}", NUM_ACTIONS, - client.getMetric().getSubmitted().getCount(), - client.getMetric().getSucceeded().getCount(), - client.getMetric().getFailed().getCount()); - assertEquals(NUM_ACTIONS, client.getMetric().getSubmitted().getCount()); - assertEquals(NUM_ACTIONS, client.getMetric().getSucceeded().getCount()); - } - } -} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java deleted file mode 100644 index abcf7a5..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportUpdateReplicaLevelTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.xbib.elasticsearch.extras.client.transport; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; - -/** - * - */ -public class BulkTransportUpdateReplicaLevelTest extends NodeTestBase { - - private static final Logger logger = LogManager.getLogger(BulkTransportUpdateReplicaLevelTest.class.getName()); - - @Test - public void testUpdateReplicaLevel() throws Exception { - - int numberOfShards = 2; - int replicaLevel = 3; - - // we need 3 nodes for replica level 3 - startNode("2"); - startNode("3"); - - int shardsAfterReplica; - - Settings settings = Settings.builder() - .put("index.number_of_shards", numberOfShards) - .put("index.number_of_replicas", 0) - .build(); - - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) - .setMetric(new SimpleBulkMetric()) - .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); - - try { - client.newIndex("replicatest", settings, null); - client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); - for (int i = 0; i < 12345; i++) { - client.index("replicatest", "replicatest", null, "{ \"name\" : \"" + randomString(32) + "\"}"); - } - client.flushIngest(); - client.waitForResponses(TimeValue.timeValueSeconds(30)); - shardsAfterReplica = client.updateReplicaLevel("replicatest", replicaLevel); - assertEquals(shardsAfterReplica, numberOfShards * (replicaLevel + 1)); - } catch (NoNodeAvailableException e) { - logger.warn("skipping, no node available"); - } finally { - client.shutdown(); - if (client.hasThrowable()) { - logger.error("error", client.getThrowable()); - } - assertFalse(client.hasThrowable()); - } - } - -} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java b/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java deleted file mode 100644 index f55c996..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Classes for testing extras for transport client. - */ -package org.xbib.elasticsearch.extras.client.transport; diff --git a/src/integration-test/java/org/xbib/elasticsearch/package-info.java b/src/integration-test/java/org/xbib/elasticsearch/package-info.java deleted file mode 100644 index 3c6e5c6..0000000 --- a/src/integration-test/java/org/xbib/elasticsearch/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Test classes for testing Elasticsearch. - */ -package org.xbib.elasticsearch; diff --git a/src/integration-test/java/suites/BulkNodeTestSuite.java b/src/integration-test/java/suites/BulkNodeTestSuite.java deleted file mode 100644 index caac820..0000000 --- a/src/integration-test/java/suites/BulkNodeTestSuite.java +++ /dev/null @@ -1,23 +0,0 @@ -package suites; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.xbib.elasticsearch.extras.client.node.BulkNodeClientTest; -import org.xbib.elasticsearch.extras.client.node.BulkNodeDuplicateIDTest; -import org.xbib.elasticsearch.extras.client.node.BulkNodeIndexAliasTest; -import org.xbib.elasticsearch.extras.client.node.BulkNodeReplicaTest; -import org.xbib.elasticsearch.extras.client.node.BulkNodeUpdateReplicaLevelTest; - -/** - * - */ -@RunWith(ListenerSuite.class) -@Suite.SuiteClasses({ - BulkNodeClientTest.class, - BulkNodeDuplicateIDTest.class, - BulkNodeReplicaTest.class, - BulkNodeUpdateReplicaLevelTest.class, - BulkNodeIndexAliasTest.class -}) -public class BulkNodeTestSuite { -} diff --git a/src/integration-test/java/suites/BulkTransportTestSuite.java b/src/integration-test/java/suites/BulkTransportTestSuite.java deleted file mode 100644 index f429dfc..0000000 --- a/src/integration-test/java/suites/BulkTransportTestSuite.java +++ /dev/null @@ -1,22 +0,0 @@ -package suites; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.xbib.elasticsearch.extras.client.transport.BulkTransportClientTest; -import org.xbib.elasticsearch.extras.client.transport.BulkTransportDuplicateIDTest; -import org.xbib.elasticsearch.extras.client.transport.BulkTransportReplicaTest; -import org.xbib.elasticsearch.extras.client.transport.BulkTransportUpdateReplicaLevelTest; - -/** - * - */ -@RunWith(ListenerSuite.class) -@Suite.SuiteClasses({ - BulkTransportClientTest.class, - BulkTransportDuplicateIDTest.class, - BulkTransportReplicaTest.class, - BulkTransportUpdateReplicaLevelTest.class -}) -public class BulkTransportTestSuite { - -} diff --git a/src/integration-test/java/suites/ListenerSuite.java b/src/integration-test/java/suites/ListenerSuite.java deleted file mode 100644 index 4101890..0000000 --- a/src/integration-test/java/suites/ListenerSuite.java +++ /dev/null @@ -1,26 +0,0 @@ -package suites; - -import org.junit.runner.Runner; -import org.junit.runner.notification.RunNotifier; -import org.junit.runners.Suite; -import org.junit.runners.model.InitializationError; -import org.junit.runners.model.RunnerBuilder; - -/** - * - */ -public class ListenerSuite extends Suite { - - private final TestListener listener = new TestListener(); - - public ListenerSuite(Class klass, RunnerBuilder builder) throws InitializationError { - super(klass, builder); - } - - @Override - protected void runChild(Runner runner, RunNotifier notifier) { - notifier.addListener(listener); - runner.run(notifier); - notifier.removeListener(listener); - } -} diff --git a/src/integration-test/java/suites/MiscTestSuite.java b/src/integration-test/java/suites/MiscTestSuite.java deleted file mode 100644 index f832e88..0000000 --- a/src/integration-test/java/suites/MiscTestSuite.java +++ /dev/null @@ -1,21 +0,0 @@ -package suites; - -import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.xbib.elasticsearch.extras.client.AliasTest; -import org.xbib.elasticsearch.extras.client.SearchTest; -import org.xbib.elasticsearch.extras.client.SimpleTest; -import org.xbib.elasticsearch.extras.client.WildcardTest; - -/** - * - */ -@RunWith(ListenerSuite.class) -@Suite.SuiteClasses({ - SimpleTest.class, - AliasTest.class, - SearchTest.class, - WildcardTest.class -}) -public class MiscTestSuite { -} diff --git a/src/integration-test/java/suites/TestListener.java b/src/integration-test/java/suites/TestListener.java deleted file mode 100644 index 7e24527..0000000 --- a/src/integration-test/java/suites/TestListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package suites; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.runner.Description; -import org.junit.runner.Result; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunListener; - -/** - * - */ -public class TestListener extends RunListener { - - private static final Logger logger = LogManager.getLogger("test.listener"); - - public void testRunStarted(Description description) throws java.lang.Exception { - logger.info("number of tests to execute: {}", description.testCount()); - } - - public void testRunFinished(Result result) throws java.lang.Exception { - logger.info("number of tests executed: {}", result.getRunCount()); - } - - public void testStarted(Description description) throws java.lang.Exception { - logger.info("starting execution of {} {}", - description.getClassName(), description.getMethodName()); - } - - public void testFinished(Description description) throws java.lang.Exception { - logger.info("finished execution of {} {}", - description.getClassName(), description.getMethodName()); - } - - public void testFailure(Failure failure) throws java.lang.Exception { - logger.info("failed execution of tests: {}", - failure.getMessage()); - } - - public void testIgnored(Description description) throws java.lang.Exception { - logger.info("execution of test ignored: {}", - description.getClassName(), description.getMethodName()); - } -} diff --git a/src/integration-test/java/suites/package-info.java b/src/integration-test/java/suites/package-info.java deleted file mode 100644 index a878170..0000000 --- a/src/integration-test/java/suites/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Test suites. - */ -package suites; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java deleted file mode 100644 index 7f8edb0..0000000 --- a/src/main/java/org/xbib/elasticsearch/extras/client/node/BulkNodeClient.java +++ /dev/null @@ -1,519 +0,0 @@ -package org.xbib.elasticsearch.extras.client.node; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.admin.indices.create.CreateIndexAction; -import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; -import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; -import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder; -import org.elasticsearch.action.bulk.BulkItemResponse; -import org.elasticsearch.action.bulk.BulkProcessor; -import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.action.delete.DeleteRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.Client; -import org.elasticsearch.client.ElasticsearchClient; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.env.Environment; -import org.elasticsearch.node.Node; -import org.elasticsearch.node.NodeValidationException; -import org.elasticsearch.plugins.Plugin; -import org.xbib.elasticsearch.extras.client.AbstractClient; -import org.xbib.elasticsearch.extras.client.BulkControl; -import org.xbib.elasticsearch.extras.client.BulkMetric; -import org.xbib.elasticsearch.extras.client.ClientMethods; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -/** - * - */ -public class BulkNodeClient extends AbstractClient implements ClientMethods { - - private static final Logger logger = LogManager.getLogger(BulkNodeClient.class.getName()); - - private int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; - - private int maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS; - - private ByteSizeValue maxVolume = DEFAULT_MAX_VOLUME_PER_REQUEST; - - private TimeValue flushInterval = DEFAULT_FLUSH_INTERVAL; - - private Node node; - - private ElasticsearchClient client; - - private BulkProcessor bulkProcessor; - - private BulkMetric metric; - - private BulkControl control; - - private Throwable throwable; - - private boolean closed; - - @Override - public BulkNodeClient maxActionsPerRequest(int maxActionsPerRequest) { - this.maxActionsPerRequest = maxActionsPerRequest; - return this; - } - - @Override - public BulkNodeClient maxConcurrentRequests(int maxConcurrentRequests) { - this.maxConcurrentRequests = maxConcurrentRequests; - return this; - } - - @Override - public BulkNodeClient maxVolumePerRequest(ByteSizeValue maxVolume) { - this.maxVolume = maxVolume; - return this; - } - - @Override - public BulkNodeClient flushIngestInterval(TimeValue flushInterval) { - this.flushInterval = flushInterval; - return this; - } - - @Override - public BulkNodeClient init(ElasticsearchClient client, - final BulkMetric metric, final BulkControl control) { - this.client = client; - this.metric = metric; - this.control = control; - if (metric != null) { - metric.start(); - } - BulkProcessor.Listener listener = new BulkProcessor.Listener() { - - private final Logger logger = LogManager.getLogger(BulkNodeClient.class.getName() + ".Listener"); - - @Override - public void beforeBulk(long executionId, BulkRequest request) { - long l = -1; - if (metric != null) { - metric.getCurrentIngest().inc(); - l = metric.getCurrentIngest().getCount(); - int n = request.numberOfActions(); - metric.getSubmitted().inc(n); - metric.getCurrentIngestNumDocs().inc(n); - metric.getTotalIngestSizeInBytes().inc(request.estimatedSizeInBytes()); - } - logger.debug("before bulk [{}] [actions={}] [bytes={}] [concurrent requests={}]", - executionId, - request.numberOfActions(), - request.estimatedSizeInBytes(), - l); - } - - @Override - public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { - long l = -1; - if (metric != null) { - metric.getCurrentIngest().dec(); - l = metric.getCurrentIngest().getCount(); - metric.getSucceeded().inc(response.getItems().length); - } - int n = 0; - for (BulkItemResponse itemResponse : response.getItems()) { - if (metric != null) { - metric.getCurrentIngest().dec(itemResponse.getIndex(), itemResponse.getType(), itemResponse.getId()); - } - if (itemResponse.isFailed()) { - n++; - if (metric != null) { - metric.getSucceeded().dec(1); - metric.getFailed().inc(1); - } - } - } - if (metric != null) { - logger.debug("after bulk [{}] [succeeded={}] [failed={}] [{}ms] {} concurrent requests", - executionId, - metric.getSucceeded().getCount(), - metric.getFailed().getCount(), - response.getTook().millis(), - l); - } - if (n > 0) { - logger.error("bulk [{}] failed with {} failed items, failure message = {}", - executionId, n, response.buildFailureMessage()); - } else { - if (metric != null) { - metric.getCurrentIngestNumDocs().dec(response.getItems().length); - } - } - } - - @Override - public void afterBulk(long executionId, BulkRequest request, Throwable failure) { - if (metric != null) { - metric.getCurrentIngest().dec(); - } - throwable = failure; - closed = true; - logger.error("after bulk [" + executionId + "] error", failure); - } - }; - BulkProcessor.Builder builder = BulkProcessor.builder((Client) client, listener) - .setBulkActions(maxActionsPerRequest) - .setConcurrentRequests(maxConcurrentRequests) - .setFlushInterval(flushInterval); - if (maxVolume != null) { - builder.setBulkSize(maxVolume); - } - this.bulkProcessor = builder.build(); - this.closed = false; - return this; - } - - @Override - public BulkNodeClient init(Settings settings, BulkMetric metric, BulkControl control) throws IOException { - createClient(settings); - this.metric = metric; - this.control = control; - return this; - } - - @Override - public ElasticsearchClient client() { - return client; - } - - @Override - protected synchronized void createClient(Settings settings) throws IOException { - if (client != null) { - logger.warn("client is open, closing..."); - client.threadPool().shutdown(); - client = null; - node.close(); - } - if (settings != null) { - String version = System.getProperty("os.name") - + " " + System.getProperty("java.vm.name") - + " " + System.getProperty("java.vm.vendor") - + " " + System.getProperty("java.runtime.version") - + " " + System.getProperty("java.vm.version"); - Settings effectiveSettings = Settings.builder().put(settings) - .put("node.client", true) - .put("node.master", false) - .put("node.data", false).build(); - logger.info("creating node client on {} with effective settings {}", - version, effectiveSettings.getAsMap()); - Collection> plugins = Collections.emptyList(); - this.node = new BulkNode(new Environment(effectiveSettings), plugins); - try { - node.start(); - } catch (NodeValidationException e) { - throw new IOException(e); - } - this.client = node.client(); - } - } - - @Override - public BulkMetric getMetric() { - return metric; - } - - @Override - public BulkNodeClient index(String index, String type, String id, String source) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new IndexRequest(index).type(type).id(id).create(false).source(source, XContentType.JSON)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of index request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient bulkIndex(IndexRequest indexRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(indexRequest.index(), indexRequest.type(), indexRequest.id()); - } - bulkProcessor.add(indexRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of index request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient delete(String index, String type, String id) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new DeleteRequest(index).type(type).id(id)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of delete failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient bulkDelete(DeleteRequest deleteRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(deleteRequest.index(), deleteRequest.type(), deleteRequest.id()); - } - bulkProcessor.add(deleteRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of delete failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient update(String index, String type, String id, String source) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of update request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient bulkUpdate(UpdateRequest updateRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(updateRequest.index(), updateRequest.type(), updateRequest.id()); - } - bulkProcessor.add(updateRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of update request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkNodeClient flushIngest() { - if (closed) { - throwClose(); - } - logger.debug("flushing bulk processor"); - bulkProcessor.flush(); - return this; - } - - @Override - public BulkNodeClient waitForResponses(TimeValue maxWaitTime) throws InterruptedException, ExecutionException { - if (closed) { - throwClose(); - } - while (!bulkProcessor.awaitClose(maxWaitTime.getMillis(), TimeUnit.MILLISECONDS)) { - logger.warn("still waiting for responses"); - } - return this; - } - - @Override - public BulkNodeClient startBulk(String index, long startRefreshIntervalMillis, long stopRefreshItervalMillis) - throws IOException { - if (control == null) { - return this; - } - if (!control.isBulk(index)) { - control.startBulk(index, startRefreshIntervalMillis, stopRefreshItervalMillis); - updateIndexSetting(index, "refresh_interval", startRefreshIntervalMillis + "ms"); - } - return this; - } - - @Override - public BulkNodeClient stopBulk(String index) throws IOException { - if (control == null) { - return this; - } - if (control.isBulk(index)) { - updateIndexSetting(index, "refresh_interval", control.getStopBulkRefreshIntervals().get(index) + "ms"); - control.finishBulk(index); - } - return this; - } - - @Override - public synchronized void shutdown() { - try { - if (bulkProcessor != null) { - logger.debug("closing bulk processor..."); - bulkProcessor.close(); - } - if (control != null && control.indices() != null && !control.indices().isEmpty()) { - logger.debug("stopping bulk mode for indices {}...", control.indices()); - for (String index : control.indices()) { - stopBulk(index); - } - metric.stop(); - } - if (node != null) { - logger.debug("closing node..."); - node.close(); - } - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - } - - @Override - public BulkNodeClient newIndex(String index) { - return newIndex(index, null, null); - } - - @Override - public BulkNodeClient newIndex(String index, String type, InputStream settings, InputStream mappings) throws IOException { - resetSettings(); - setting(settings); - mapping(type, mappings); - return newIndex(index, settings(), mappings()); - } - - @Override - public BulkNodeClient newIndex(String index, Settings settings, Map mappings) { - if (closed) { - throwClose(); - } - if (client == null) { - logger.warn("no client for create index"); - return this; - } - if (index == null) { - logger.warn("no index name given to create index"); - return this; - } - CreateIndexRequestBuilder createIndexRequestBuilder = - new CreateIndexRequestBuilder(client(), CreateIndexAction.INSTANCE).setIndex(index); - if (settings != null) { - logger.info("settings = {}", settings.getAsStructuredMap()); - createIndexRequestBuilder.setSettings(settings); - } - if (mappings != null) { - for (Map.Entry entry : mappings.entrySet()) { - String type = entry.getKey(); - String mapping = entry.getValue(); - logger.info("found mapping for {}", type); - createIndexRequestBuilder.addMapping(type, mapping, XContentType.JSON); - } - } - createIndexRequestBuilder.execute().actionGet(); - logger.info("index {} created", index); - return this; - } - - @Override - public BulkNodeClient newMapping(String index, String type, Map mapping) { - PutMappingRequestBuilder putMappingRequestBuilder = - new PutMappingRequestBuilder(client(), PutMappingAction.INSTANCE) - .setIndices(index) - .setType(type) - .setSource(mapping); - putMappingRequestBuilder.execute().actionGet(); - logger.info("mapping created for index {} and type {}", index, type); - return this; - } - - @Override - public BulkNodeClient deleteIndex(String index) { - if (closed) { - throwClose(); - } - if (client == null) { - logger.warn("no client"); - return this; - } - if (index == null) { - logger.warn("no index name given to delete index"); - return this; - } - DeleteIndexRequestBuilder deleteIndexRequestBuilder = - new DeleteIndexRequestBuilder(client(), DeleteIndexAction.INSTANCE, index); - deleteIndexRequestBuilder.execute().actionGet(); - return this; - } - - @Override - public boolean hasThrowable() { - return throwable != null; - } - - @Override - public Throwable getThrowable() { - return throwable; - } - - public Settings getSettings() { - return settings(); - } - - @Override - public Settings.Builder getSettingsBuilder() { - return settingsBuilder(); - } - - private static void throwClose() { - throw new ElasticsearchException("client is closed"); - } - - private class BulkNode extends Node { - - BulkNode(Environment env, Collection> classpathPlugins) { - super(env, classpathPlugins); - } - } - -} diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/package-info.java b/src/main/java/org/xbib/elasticsearch/extras/client/package-info.java deleted file mode 100644 index c231c60..0000000 --- a/src/main/java/org/xbib/elasticsearch/extras/client/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Classes for Elasticsearch client extras. - */ -package org.xbib.elasticsearch.extras.client; diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java deleted file mode 100644 index 2a8193f..0000000 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClient.java +++ /dev/null @@ -1,588 +0,0 @@ -package org.xbib.elasticsearch.extras.client.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; -import org.elasticsearch.action.admin.cluster.state.ClusterStateRequestBuilder; -import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; -import org.elasticsearch.action.admin.indices.create.CreateIndexAction; -import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; -import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; -import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequestBuilder; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder; -import org.elasticsearch.action.bulk.BulkItemResponse; -import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.action.delete.DeleteRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.ElasticsearchClient; -import org.elasticsearch.client.transport.NoNodeAvailableException; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.common.network.NetworkModule; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.InetSocketTransportAddress; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.transport.Netty4Plugin; -import org.xbib.elasticsearch.extras.client.AbstractClient; -import org.xbib.elasticsearch.extras.client.BulkControl; -import org.xbib.elasticsearch.extras.client.BulkMetric; -import org.xbib.elasticsearch.extras.client.BulkProcessor; -import org.xbib.elasticsearch.extras.client.ClientMethods; -import org.xbib.elasticsearch.extras.client.NetworkUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -/** - * Transport client with addtitional methods using the BulkProcessor. - */ -public class BulkTransportClient extends AbstractClient implements ClientMethods { - - private static final Logger logger = LogManager.getLogger(BulkTransportClient.class.getName()); - - private int maxActionsPerRequest = DEFAULT_MAX_ACTIONS_PER_REQUEST; - - private int maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS; - - private ByteSizeValue maxVolumePerRequest = DEFAULT_MAX_VOLUME_PER_REQUEST; - - private TimeValue flushInterval = DEFAULT_FLUSH_INTERVAL; - - private BulkProcessor bulkProcessor; - - private Throwable throwable; - - private boolean closed; - - private TransportClient client; - - private BulkMetric metric; - - private BulkControl control; - - private boolean ignoreBulkErrors; - - private boolean isShutdown; - - @Override - public BulkTransportClient init(ElasticsearchClient client, BulkMetric metric, BulkControl control) throws IOException { - return init(findSettings(), metric, control); - } - - @Override - public BulkTransportClient init(Settings settings, final BulkMetric metric, final BulkControl control) { - createClient(settings); - this.metric = metric; - this.control = control; - if (metric != null) { - metric.start(); - } - resetSettings(); - BulkProcessor.Listener listener = new BulkProcessor.Listener() { - - private final Logger logger = LogManager.getLogger(BulkTransportClient.class.getName() + ".Listener"); - - @Override - public void beforeBulk(long executionId, BulkRequest request) { - long l = -1L; - if (metric != null) { - metric.getCurrentIngest().inc(); - l = metric.getCurrentIngest().getCount(); - int n = request.numberOfActions(); - metric.getSubmitted().inc(n); - metric.getCurrentIngestNumDocs().inc(n); - metric.getTotalIngestSizeInBytes().inc(request.estimatedSizeInBytes()); - } - logger.debug("before bulk [{}] [actions={}] [bytes={}] [concurrent requests={}]", - executionId, - request.numberOfActions(), - request.estimatedSizeInBytes(), - l); - } - - @Override - public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { - long l = -1L; - if (metric != null) { - metric.getCurrentIngest().dec(); - l = metric.getCurrentIngest().getCount(); - metric.getSucceeded().inc(response.getItems().length); - } - int n = 0; - for (BulkItemResponse itemResponse : response.getItems()) { - if (metric != null) { - metric.getCurrentIngest().dec(itemResponse.getIndex(), itemResponse.getType(), itemResponse.getId()); - if (itemResponse.isFailed()) { - n++; - metric.getSucceeded().dec(1); - metric.getFailed().inc(1); - } - } - } - if (metric != null) { - logger.debug("after bulk [{}] [succeeded={}] [failed={}] [{}ms] [concurrent requests={}]", - executionId, - metric.getSucceeded().getCount(), - metric.getFailed().getCount(), - response.getTook().millis(), - l); - } - if (n > 0) { - logger.error("bulk [{}] failed with {} failed items, failure message = {}", - executionId, n, response.buildFailureMessage()); - } else { - if (metric != null) { - metric.getCurrentIngestNumDocs().dec(response.getItems().length); - } - } - } - - @Override - public void afterBulk(long executionId, BulkRequest requst, Throwable failure) { - if (metric != null) { - metric.getCurrentIngest().dec(); - } - throwable = failure; - if (!ignoreBulkErrors) { - closed = true; - } - logger.error("bulk [" + executionId + "] error", failure); - } - }; - BulkProcessor.Builder builder = BulkProcessor.builder(client, listener) - .setBulkActions(maxActionsPerRequest) - .setConcurrentRequests(maxConcurrentRequests) - .setFlushInterval(flushInterval); - if (maxVolumePerRequest != null) { - builder.setBulkSize(maxVolumePerRequest); - } - this.bulkProcessor = builder.build(); - // auto-connect here - try { - Collection addrs = findAddresses(settings); - if (!connect(addrs, settings.getAsBoolean("autodiscover", false))) { - throw new NoNodeAvailableException("no cluster nodes available, check settings " - + settings.getAsMap()); - } - } catch (IOException e) { - logger.error(e.getMessage(), e); - } - this.closed = false; - return this; - } - - @Override - public ClientMethods newMapping(String index, String type, Map mapping) { - new PutMappingRequestBuilder(client(), PutMappingAction.INSTANCE) - .setIndices(index) - .setType(type) - .setSource(mapping) - .execute().actionGet(); - logger.info("mapping created for index {} and type {}", index, type); - return this; - } - - @Override - protected void createClient(Settings settings) { - if (client != null) { - logger.warn("client is open, closing..."); - client.close(); - client.threadPool().shutdown(); - client = null; - } - if (settings != null) { - String version = System.getProperty("os.name") - + " " + System.getProperty("java.vm.name") - + " " + System.getProperty("java.vm.vendor") - + " " + System.getProperty("java.runtime.version") - + " " + System.getProperty("java.vm.version"); - logger.info("creating transport client on {} with effective settings {}", - version, settings.getAsMap()); - this.client = new TransportClient(Settings.builder() - .put("cluster.name", settings.get("cluster.name")) - .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) - .build(), Collections.singletonList(Netty4Plugin.class)); - this.ignoreBulkErrors = settings.getAsBoolean("ignoreBulkErrors", true); - } - } - - public boolean isShutdown() { - return isShutdown; - } - - @Override - public BulkTransportClient maxActionsPerRequest(int maxActionsPerRequest) { - this.maxActionsPerRequest = maxActionsPerRequest; - return this; - } - - @Override - public BulkTransportClient maxConcurrentRequests(int maxConcurrentRequests) { - this.maxConcurrentRequests = maxConcurrentRequests; - return this; - } - - @Override - public BulkTransportClient maxVolumePerRequest(ByteSizeValue maxVolumePerRequest) { - this.maxVolumePerRequest = maxVolumePerRequest; - return this; - } - - @Override - public BulkTransportClient flushIngestInterval(TimeValue flushInterval) { - this.flushInterval = flushInterval; - return this; - } - - @Override - public ElasticsearchClient client() { - return client; - } - - @Override - public BulkMetric getMetric() { - return metric; - } - - @Override - public ClientMethods newIndex(String index) { - if (closed) { - throwClose(); - } - return newIndex(index, null, null); - } - - @Override - public ClientMethods newIndex(String index, String type, InputStream settings, InputStream mappings) throws IOException { - resetSettings(); - setting(settings); - mapping(type, mappings); - return newIndex(index, settings(), mappings()); - } - - @Override - public ClientMethods newIndex(String index, Settings settings, Map mappings) { - if (closed) { - throwClose(); - } - if (index == null) { - logger.warn("no index name given to create index"); - return this; - } - CreateIndexRequestBuilder createIndexRequestBuilder = - new CreateIndexRequestBuilder(client(), CreateIndexAction.INSTANCE).setIndex(index); - if (settings != null) { - logger.info("settings = {}", settings.getAsStructuredMap()); - createIndexRequestBuilder.setSettings(settings); - } - if (mappings != null) { - for (Map.Entry entry : mappings.entrySet()) { - String type = entry.getKey(); - String mapping = entry.getValue(); - logger.info("found mapping for {}", type); - createIndexRequestBuilder.addMapping(type, mapping, XContentType.JSON); - } - } - createIndexRequestBuilder.execute().actionGet(); - logger.info("index {} created", index); - return this; - } - - @Override - public ClientMethods deleteIndex(String index) { - if (closed) { - throwClose(); - } - if (index == null) { - logger.warn("no index name given to delete index"); - return this; - } - new DeleteIndexRequestBuilder(client(), DeleteIndexAction.INSTANCE, index).execute().actionGet(); - return this; - } - - @Override - public ClientMethods startBulk(String index, long startRefreshIntervalSeconds, long stopRefreshIntervalSeconds) - throws IOException { - if (control == null) { - return this; - } - if (!control.isBulk(index) && startRefreshIntervalSeconds > 0L && stopRefreshIntervalSeconds > 0L) { - control.startBulk(index, startRefreshIntervalSeconds, stopRefreshIntervalSeconds); - updateIndexSetting(index, "refresh_interval", startRefreshIntervalSeconds + "s"); - } - return this; - } - - @Override - public ClientMethods stopBulk(String index) throws IOException { - if (control == null) { - return this; - } - if (control.isBulk(index)) { - long secs = control.getStopBulkRefreshIntervals().get(index); - if (secs > 0L) { - updateIndexSetting(index, "refresh_interval", secs + "s"); - } - control.finishBulk(index); - } - return this; - } - - @Override - public BulkTransportClient index(String index, String type, String id, String source) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new IndexRequest().index(index).type(type).id(id).create(false) - .source(source, XContentType.JSON)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of index request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient bulkIndex(IndexRequest indexRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(indexRequest.index(), indexRequest.type(), indexRequest.id()); - } - bulkProcessor.add(indexRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of index request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient delete(String index, String type, String id) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new DeleteRequest().index(index).type(type).id(id)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of delete request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient bulkDelete(DeleteRequest deleteRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(deleteRequest.index(), deleteRequest.type(), deleteRequest.id()); - } - bulkProcessor.add(deleteRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of delete request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient update(String index, String type, String id, String source) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(index, type, id); - } - bulkProcessor.add(new UpdateRequest().index(index).type(type).id(id).upsert(source, XContentType.JSON)); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of update request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient bulkUpdate(UpdateRequest updateRequest) { - if (closed) { - throwClose(); - } - try { - if (metric != null) { - metric.getCurrentIngest().inc(updateRequest.index(), updateRequest.type(), updateRequest.id()); - } - bulkProcessor.add(updateRequest); - } catch (Exception e) { - throwable = e; - closed = true; - logger.error("bulk add of update request failed: " + e.getMessage(), e); - } - return this; - } - - @Override - public BulkTransportClient flushIngest() { - if (closed) { - throwClose(); - } - logger.debug("flushing bulk processor"); - bulkProcessor.flush(); - return this; - } - - @Override - public BulkTransportClient waitForResponses(TimeValue maxWaitTime) throws InterruptedException, ExecutionException { - if (closed) { - throwClose(); - } - if (!bulkProcessor.awaitClose(maxWaitTime.getMillis(), TimeUnit.MILLISECONDS)) { - logger.warn("still waiting for responses"); - } - return this; - } - - @Override - public synchronized void shutdown() { - if (closed) { - shutdownClient(); - throwClose(); - } - try { - if (bulkProcessor != null) { - logger.debug("closing bulk processor..."); - bulkProcessor.close(); - } - if (control != null && control.indices() != null && !control.indices().isEmpty()) { - logger.debug("stopping bulk mode for indices {}...", control.indices()); - for (String index : control.indices()) { - stopBulk(index); - } - metric.stop(); - } - logger.debug("shutting down..."); - shutdownClient(); - logger.debug("shutting down completed"); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - } - - @Override - public boolean hasThrowable() { - return throwable != null; - } - - @Override - public Throwable getThrowable() { - return throwable; - } - - private Settings findSettings() { - Settings.Builder settingsBuilder = Settings.builder(); - settingsBuilder.put("host", "localhost"); - try { - String hostname = NetworkUtils.getLocalAddress().getHostName(); - logger.debug("the hostname is {}", hostname); - settingsBuilder.put("host", hostname) - .put("port", 9300); - } catch (Exception e) { - logger.warn(e.getMessage(), e); - } - return settingsBuilder.build(); - } - - private Collection findAddresses(Settings settings) throws IOException { - String[] hostnames = settings.getAsArray("host", new String[]{"localhost"}); - int port = settings.getAsInt("port", 9300); - Collection addresses = new ArrayList<>(); - for (String hostname : hostnames) { - String[] splitHost = hostname.split(":", 2); - if (splitHost.length == 2) { - String host = splitHost[0]; - InetAddress inetAddress = NetworkUtils.resolveInetAddress(host, null); - try { - port = Integer.parseInt(splitHost[1]); - } catch (Exception e) { - logger.warn(e.getMessage(), e); - } - addresses.add(new InetSocketTransportAddress(inetAddress, port)); - } - if (splitHost.length == 1) { - String host = splitHost[0]; - InetAddress inetAddress = NetworkUtils.resolveInetAddress(host, null); - addresses.add(new InetSocketTransportAddress(inetAddress, port)); - } - } - return addresses; - } - - private static void throwClose() { - throw new ElasticsearchException("client is closed"); - } - - private void shutdownClient() { - if (client != null) { - logger.debug("shutdown started"); - client.close(); - client.threadPool().shutdown(); - client = null; - logger.debug("shutdown complete"); - } - isShutdown = true; - } - - private boolean connect(Collection addresses, boolean autodiscover) { - logger.info("trying to connect to {}", addresses); - client.addTransportAddresses(addresses); - if (client.connectedNodes() != null) { - List nodes = client.connectedNodes(); - if (!nodes.isEmpty()) { - logger.info("connected to {}", nodes); - if (autodiscover) { - logger.info("trying to auto-discover all cluster nodes..."); - ClusterStateRequestBuilder clusterStateRequestBuilder = - new ClusterStateRequestBuilder(client, ClusterStateAction.INSTANCE); - ClusterStateResponse clusterStateResponse = clusterStateRequestBuilder.execute().actionGet(); - DiscoveryNodes discoveryNodes = clusterStateResponse.getState().getNodes(); - client.addDiscoveryNodes(discoveryNodes); - logger.info("after auto-discovery connected to {}", client.connectedNodes()); - } - return true; - } - return false; - } - return false; - } -} diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/package-info.java b/src/main/java/org/xbib/elasticsearch/extras/client/transport/package-info.java deleted file mode 100644 index ac6a50d..0000000 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Classes for Elasticsearch transport client extras. - */ -package org.xbib.elasticsearch.extras.client.transport; diff --git a/transport/build.gradle b/transport/build.gradle new file mode 100644 index 0000000..8cfb1ce --- /dev/null +++ b/transport/build.gradle @@ -0,0 +1,63 @@ +buildscript { + repositories { + jcenter() + maven { + url 'http://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.2.2.0" + } +} + +apply plugin: 'org.xbib.gradle.plugin.elasticsearch.build' + +configurations { + main + tests +} + +dependencies { + compile project(':common') + testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" + testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" +} + +jar { + baseName "${rootProject.name}-transport" +} + +task testJar(type: Jar, dependsOn: testClasses) { + baseName = "${project.archivesBaseName}-tests" + from sourceSets.test.output +} + +artifacts { + main jar + tests testJar + archives sourcesJar, javadocJar +} + +esTest { + enabled = true + // test with the jars, not the classes, for security manager + classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files + systemProperty 'tests.security.manager', 'true' + // maybe we like some extra security policy for our code + systemProperty 'tests.security.policy', '/extra-security.policy' +} +esTest.dependsOn jar, testJar + +randomizedTest { + enabled = false +} + +test { + enabled = false + jvmArgs "-javaagent:" + configurations.alpnagent.asPath + systemProperty 'path.home', projectDir.absolutePath + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} \ No newline at end of file diff --git a/transport/config/checkstyle/checkstyle.xml b/transport/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/transport/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/transport/src/docs/asciidoc/css/foundation.css b/transport/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/transport/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/transport/src/docs/asciidoclet/overview.adoc b/transport/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..7947331 --- /dev/null +++ b/transport/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Elasticsearch Java client +Jörg Prante +Version 5.4.0.0 + diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufBytesReference.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufBytesReference.java new file mode 100644 index 0000000..e0b96d6 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufBytesReference.java @@ -0,0 +1,74 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +final class ByteBufBytesReference extends BytesReference { + + private final ByteBuf buffer; + private final int length; + private final int offset; + + ByteBufBytesReference(ByteBuf buffer, int length) { + this.buffer = buffer; + this.length = length; + this.offset = buffer.readerIndex(); + assert length <= buffer.readableBytes() : "length[" + length +"] > " + buffer.readableBytes(); + } + + @Override + public byte get(int index) { + return buffer.getByte(offset + index); + } + + @Override + public int length() { + return length; + } + + @Override + public BytesReference slice(int from, int length) { + return new ByteBufBytesReference(buffer.slice(offset + from, length), length); + } + + @Override + public StreamInput streamInput() { + return new ByteBufStreamInput(buffer.duplicate(), length); + } + + @Override + public void writeTo(OutputStream os) throws IOException { + buffer.getBytes(offset, os, length); + } + + ByteBuf toByteBuf() { + return buffer.duplicate(); + } + + @Override + public String utf8ToString() { + return buffer.toString(offset, length, StandardCharsets.UTF_8); + } + + @Override + public BytesRef toBytesRef() { + if (buffer.hasArray()) { + return new BytesRef(buffer.array(), buffer.arrayOffset() + offset, length); + } + final byte[] copy = new byte[length]; + buffer.getBytes(offset, copy); + return new BytesRef(copy); + } + + @Override + public long ramBytesUsed() { + return buffer.capacity(); + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufStreamInput.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufStreamInput.java new file mode 100644 index 0000000..1dadaea --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ByteBufStreamInput.java @@ -0,0 +1,131 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.EOFException; +import java.io.IOException; + +/** + * A Netty {@link io.netty.buffer.ByteBuf} based {@link org.elasticsearch.common.io.stream.StreamInput}. + */ +class ByteBufStreamInput extends StreamInput { + + private final ByteBuf buffer; + private final int endIndex; + + ByteBufStreamInput(ByteBuf buffer, int length) { + if (length > buffer.readableBytes()) { + throw new IndexOutOfBoundsException(); + } + this.buffer = buffer; + int startIndex = buffer.readerIndex(); + endIndex = startIndex + length; + buffer.markReaderIndex(); + } + + @Override + public BytesReference readBytesReference(int length) throws IOException { + // NOTE: It is unsafe to share a reference of the internal structure, so we + // use the default implementation which will copy the bytes. It is unsafe because + // a netty ByteBuf might be pooled which requires a manual release to prevent + // memory leaks. + return super.readBytesReference(length); + } + + @Override + public BytesRef readBytesRef(int length) throws IOException { + // NOTE: It is unsafe to share a reference of the internal structure, so we + // use the default implementation which will copy the bytes. It is unsafe because + // a netty ByteBuf might be pooled which requires a manual release to prevent + // memory leaks. + return super.readBytesRef(length); + } + + @Override + public int available() throws IOException { + return endIndex - buffer.readerIndex(); + } + + @Override + protected void ensureCanReadBytes(int length) throws EOFException { + int bytesAvailable = endIndex - buffer.readerIndex(); + if (bytesAvailable < length) { + throw new EOFException("tried to read: " + length + " bytes but only " + bytesAvailable + " remaining"); + } + } + + @Override + public void mark(int readlimit) { + buffer.markReaderIndex(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public int read() throws IOException { + if (available() == 0) { + return -1; + } + return buffer.readByte() & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int available = available(); + if (available == 0) { + return -1; + } + + len = Math.min(available, len); + buffer.readBytes(b, off, len); + return len; + } + + @Override + public void reset() throws IOException { + buffer.resetReaderIndex(); + } + + @Override + public long skip(long n) throws IOException { + if (n > Integer.MAX_VALUE) { + return skipBytes(Integer.MAX_VALUE); + } else { + return skipBytes((int) n); + } + } + + public int skipBytes(int n) throws IOException { + int nBytes = Math.min(available(), n); + buffer.skipBytes(nBytes); + return nBytes; + } + + + @Override + public byte readByte() throws IOException { + return buffer.readByte(); + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException { + int read = read(b, offset, len); + if (read < len) { + throw new IndexOutOfBoundsException(); + } + } + + @Override + public void close() throws IOException { + // nothing to do here + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/CompressibleBytesOutputStream.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/CompressibleBytesOutputStream.java new file mode 100644 index 0000000..3318068 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/CompressibleBytesOutputStream.java @@ -0,0 +1,87 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressorFactory; +import org.elasticsearch.common.io.Streams; +import org.elasticsearch.common.io.stream.BytesStream; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.zip.DeflaterOutputStream; + +/** + * This class exists to provide a stream with optional compression. This is useful as using compression + * requires that the underlying {@link DeflaterOutputStream} be closed to write EOS bytes. However, the + * {@link BytesStream} should not be closed yet, as we have not used the bytes. This class handles these + * intricacies. + * + * {@link CompressibleBytesOutputStream#materializeBytes()} should be called when all the bytes have been + * written to this stream. If compression is enabled, the proper EOS bytes will be written at that point. + * The underlying {@link BytesReference} will be returned. + * + * {@link CompressibleBytesOutputStream#close()} should be called when the bytes are no longer needed and + * can be safely released. + */ +final class CompressibleBytesOutputStream extends StreamOutput { + + private final StreamOutput stream; + private final BytesStream bytesStreamOutput; + private final boolean shouldCompress; + + CompressibleBytesOutputStream(BytesStream bytesStreamOutput, boolean shouldCompress) throws IOException { + this.bytesStreamOutput = bytesStreamOutput; + this.shouldCompress = shouldCompress; + if (shouldCompress) { + this.stream = CompressorFactory.COMPRESSOR.streamOutput(Streams.flushOnCloseStream(bytesStreamOutput)); + } else { + this.stream = bytesStreamOutput; + } + } + + /** + * This method ensures that compression is complete and returns the underlying bytes. + * + * @return bytes underlying the stream + * @throws IOException if an exception occurs when writing or flushing + */ + BytesReference materializeBytes() throws IOException { + // If we are using compression the stream needs to be closed to ensure that EOS marker bytes are written. + // The actual ReleasableBytesStreamOutput will not be closed yet as it is wrapped in flushOnCloseStream when + // passed to the deflater stream. + if (shouldCompress) { + stream.close(); + } + + return bytesStreamOutput.bytes(); + } + + @Override + public void writeByte(byte b) throws IOException { + stream.write(b); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException { + stream.writeBytes(b, offset, length); + } + + @Override + public void flush() throws IOException { + stream.flush(); + } + + @Override + public void close() throws IOException { + if (stream == bytesStreamOutput) { + IOUtils.close(stream); + } else { + IOUtils.close(stream, bytesStreamOutput); + } + } + + @Override + public void reset() { + throw new UnsupportedOperationException(); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/ConnectionProfile.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ConnectionProfile.java new file mode 100644 index 0000000..dd8cc84 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ConnectionProfile.java @@ -0,0 +1,209 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.common.inject.internal.Nullable; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.transport.TransportRequestOptions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A connection profile describes how many connection are established to specific node for each of the available request types. + * ({@link org.elasticsearch.transport.TransportRequestOptions.Type}). This allows to tailor a connection towards a specific usage. + */ +public final class ConnectionProfile { + + /** + * Builds a connection profile that is dedicated to a single channel type. Use this + * when opening single use connections + */ + public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType, + @Nullable TimeValue connectTimeout, + @Nullable TimeValue handshakeTimeout) { + Builder builder = new Builder(); + builder.addConnections(1, channelType); + final EnumSet otherTypes = EnumSet.allOf(TransportRequestOptions.Type.class); + otherTypes.remove(channelType); + builder.addConnections(0, otherTypes.stream().toArray(TransportRequestOptions.Type[]::new)); + if (connectTimeout != null) { + builder.setConnectTimeout(connectTimeout); + } + if (handshakeTimeout != null) { + builder.setHandshakeTimeout(handshakeTimeout); + } + return builder.build(); + } + + private final List handles; + private final int numConnections; + private final TimeValue connectTimeout; + private final TimeValue handshakeTimeout; + + private ConnectionProfile(List handles, int numConnections, TimeValue connectTimeout, TimeValue handshakeTimeout) + { + this.handles = handles; + this.numConnections = numConnections; + this.connectTimeout = connectTimeout; + this.handshakeTimeout = handshakeTimeout; + } + + /** + * A builder to build a new {@link ConnectionProfile} + */ + public static class Builder { + private final List handles = new ArrayList<>(); + private final Set addedTypes = EnumSet.noneOf(TransportRequestOptions.Type.class); + private int offset = 0; + private TimeValue connectTimeout; + private TimeValue handshakeTimeout; + + /** create an empty builder */ + public Builder() { + } + + /** copy constructor, using another profile as a base */ + public Builder(ConnectionProfile source) { + handles.addAll(source.getHandles()); + offset = source.getNumConnections(); + handles.forEach(th -> addedTypes.addAll(th.types)); + connectTimeout = source.getConnectTimeout(); + handshakeTimeout = source.getHandshakeTimeout(); + } + /** + * Sets a connect timeout for this connection profile + */ + public void setConnectTimeout(TimeValue connectTimeout) { + if (connectTimeout.millis() < 0) { + throw new IllegalArgumentException("connectTimeout must be non-negative but was: " + connectTimeout); + } + this.connectTimeout = connectTimeout; + } + + /** + * Sets a handshake timeout for this connection profile + */ + public void setHandshakeTimeout(TimeValue handshakeTimeout) { + if (handshakeTimeout.millis() < 0) { + throw new IllegalArgumentException("handshakeTimeout must be non-negative but was: " + handshakeTimeout); + } + this.handshakeTimeout = handshakeTimeout; + } + + /** + * Adds a number of connections for one or more types. Each type can only be added once. + * @param numConnections the number of connections to use in the pool for the given connection types + * @param types a set of types that should share the given number of connections + */ + public void addConnections(int numConnections, TransportRequestOptions.Type... types) { + if (types == null || types.length == 0) { + throw new IllegalArgumentException("types must not be null"); + } + for (TransportRequestOptions.Type type : types) { + if (addedTypes.contains(type)) { + throw new IllegalArgumentException("type [" + type + "] is already registered"); + } + } + addedTypes.addAll(Arrays.asList(types)); + handles.add(new ConnectionTypeHandle(offset, numConnections, EnumSet.copyOf(Arrays.asList(types)))); + offset += numConnections; + } + + /** + * Creates a new {@link ConnectionProfile} based on the added connections. + * @throws IllegalStateException if any of the {@link org.elasticsearch.transport.TransportRequestOptions.Type} enum is missing + */ + public ConnectionProfile build() { + EnumSet types = EnumSet.allOf(TransportRequestOptions.Type.class); + types.removeAll(addedTypes); + if (types.isEmpty() == false) { + throw new IllegalStateException("not all types are added for this connection profile - missing types: " + types); + } + return new ConnectionProfile(Collections.unmodifiableList(handles), offset, connectTimeout, handshakeTimeout); + } + + } + + /** + * Returns the connect timeout or null if no explicit timeout is set on this profile. + */ + public TimeValue getConnectTimeout() { + return connectTimeout; + } + + /** + * Returns the handshake timeout or null if no explicit timeout is set on this profile. + */ + public TimeValue getHandshakeTimeout() { + return handshakeTimeout; + } + + /** + * Returns the total number of connections for this profile + */ + public int getNumConnections() { + return numConnections; + } + + /** + * Returns the number of connections per type for this profile. This might return a count that is shared with other types such + * that the sum of all connections per type might be higher than {@link #getNumConnections()}. For instance if + * {@link org.elasticsearch.transport.TransportRequestOptions.Type#BULK} shares connections with + * {@link org.elasticsearch.transport.TransportRequestOptions.Type#REG} they will return both the same number of connections from + * this method but the connections are not distinct. + */ + public int getNumConnectionsPerType(TransportRequestOptions.Type type) { + for (ConnectionTypeHandle handle : handles) { + if (handle.getTypes().contains(type)) { + return handle.length; + } + } + throw new AssertionError("no handle found for type: " + type); + } + + /** + * Returns the type handles for this connection profile + */ + List getHandles() { + return Collections.unmodifiableList(handles); + } + + /** + * Connection type handle encapsulates the logic which connection + */ + static final class ConnectionTypeHandle { + public final int length; + public final int offset; + private final Set types; + private final AtomicInteger counter = new AtomicInteger(); + + private ConnectionTypeHandle(int offset, int length, Set types) { + this.length = length; + this.offset = offset; + this.types = types; + } + + /** + * Returns one of the channels out configured for this handle. The channel is selected in a round-robin + * fashion. + */ + T getChannel(List channels) { + if (length == 0) { + throw new IllegalStateException("can't select channel size is 0 for types: " + types); + } + assert channels.size() >= offset + length : "illegal size: " + channels.size() + " expected >= " + (offset + length); + return channels.get(offset + Math.floorMod(counter.incrementAndGet(), length)); + } + + /** + * Returns all types for this handle + */ + Set getTypes() { + return types; + } + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/ESLoggingHandler.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ESLoggingHandler.java new file mode 100644 index 0000000..5ed6ef6 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/ESLoggingHandler.java @@ -0,0 +1,108 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import org.elasticsearch.Version; +import org.elasticsearch.common.compress.Compressor; +import org.elasticsearch.common.compress.CompressorFactory; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.transport.TcpHeader; +import org.elasticsearch.transport.TransportStatus; + +import java.io.IOException; + +final class ESLoggingHandler extends LoggingHandler { + + ESLoggingHandler() { + super(LogLevel.TRACE); + } + + @Override + protected String format(final ChannelHandlerContext ctx, final String eventName, final Object arg) { + if (arg instanceof ByteBuf) { + try { + return format(ctx, eventName, (ByteBuf) arg); + } catch (final Exception e) { + // we really do not want to allow a bug in the formatting handling to escape + logger.trace("an exception occurred formatting a trace message", e); + // we are going to let this be formatted via the default formatting + return super.format(ctx, eventName, arg); + } + } else { + return super.format(ctx, eventName, arg); + } + } + + private static final int MESSAGE_LENGTH_OFFSET = TcpHeader.MARKER_BYTES_SIZE; + private static final int REQUEST_ID_OFFSET = MESSAGE_LENGTH_OFFSET + TcpHeader.MESSAGE_LENGTH_SIZE; + private static final int STATUS_OFFSET = REQUEST_ID_OFFSET + TcpHeader.REQUEST_ID_SIZE; + private static final int VERSION_ID_OFFSET = STATUS_OFFSET + TcpHeader.STATUS_SIZE; + private static final int ACTION_OFFSET = VERSION_ID_OFFSET + TcpHeader.VERSION_ID_SIZE; + + private String format(final ChannelHandlerContext ctx, final String eventName, final ByteBuf arg) throws IOException { + final int readableBytes = arg.readableBytes(); + if (readableBytes == 0) { + return super.format(ctx, eventName, arg); + } else if (readableBytes >= 2) { + final StringBuilder sb = new StringBuilder(); + sb.append(ctx.channel().toString()); + final int offset = arg.readerIndex(); + // this might be an ES message, check the header + if (arg.getByte(offset) == (byte) 'E' && arg.getByte(offset + 1) == (byte) 'S') { + if (readableBytes == TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE) { + final int length = arg.getInt(offset + MESSAGE_LENGTH_OFFSET); + if (length == TcpTransport.PING_DATA_SIZE) { + sb.append(" [ping]").append(' ').append(eventName).append(": ").append(readableBytes).append('B'); + return sb.toString(); + } + } + else if (readableBytes >= TcpHeader.HEADER_SIZE) { + // we are going to try to decode this as an ES message + final int length = arg.getInt(offset + MESSAGE_LENGTH_OFFSET); + final long requestId = arg.getLong(offset + REQUEST_ID_OFFSET); + final byte status = arg.getByte(offset + STATUS_OFFSET); + final boolean isRequest = TransportStatus.isRequest(status); + final String type = isRequest ? "request" : "response"; + final String version = Version.fromId(arg.getInt(offset + VERSION_ID_OFFSET)).toString(); + sb.append(" [length: ").append(length); + sb.append(", request id: ").append(requestId); + sb.append(", type: ").append(type); + sb.append(", version: ").append(version); + if (isRequest) { + // it looks like an ES request, try to decode the action + final int remaining = readableBytes - ACTION_OFFSET; + final ByteBuf slice = arg.slice(offset + ACTION_OFFSET, remaining); + // the stream might be compressed + try (StreamInput in = in(status, slice, remaining)) { + // the first bytes in the message is the context headers + try (ThreadContext context = new ThreadContext(Settings.EMPTY)) { + context.readHeaders(in); + } + // now we can decode the action name + sb.append(", action: ").append(in.readString()); + } + } + sb.append(']'); + sb.append(' ').append(eventName).append(": ").append(readableBytes).append('B'); + return sb.toString(); + } + } + } + // we could not decode this as an ES message, use the default formatting + return super.format(ctx, eventName, arg); + } + + private StreamInput in(final Byte status, final ByteBuf slice, final int remaining) throws IOException { + final ByteBufStreamInput in = new ByteBufStreamInput(slice, remaining); + if (TransportStatus.isCompress(status)) { + final Compressor compressor = CompressorFactory.compressor(Netty4Utils.toBytesReference(slice)); + return compressor.streamInput(in); + } else { + return in; + } + } +} diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/MockTransportBulkClient.java similarity index 52% rename from src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java rename to transport/src/main/java/org/xbib/elasticsearch/client/transport/MockTransportBulkClient.java index a25d012..04321a8 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/MockTransportClient.java +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/MockTransportBulkClient.java @@ -1,6 +1,5 @@ -package org.xbib.elasticsearch.extras.client.transport; +package org.xbib.elasticsearch.client.transport; -import org.elasticsearch.action.bulk.BulkProcessor; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.update.UpdateRequest; @@ -8,8 +7,8 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.xbib.elasticsearch.extras.client.BulkControl; -import org.xbib.elasticsearch.extras.client.BulkMetric; +import org.xbib.elasticsearch.client.BulkControl; +import org.xbib.elasticsearch.client.BulkMetric; import java.io.IOException; import java.util.Map; @@ -18,7 +17,7 @@ import java.util.Map; * Mock client, it does not perform actions on a cluster. * Useful for testing or dry runs. */ -public class MockTransportClient extends BulkTransportClient { +public class MockTransportBulkClient extends TransportBulkClient { @Override public ElasticsearchClient client() { @@ -26,97 +25,92 @@ public class MockTransportClient extends BulkTransportClient { } @Override - public MockTransportClient init(ElasticsearchClient client, BulkMetric metric, BulkControl control) { + public MockTransportBulkClient init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control) { return this; } @Override - public MockTransportClient init(Settings settings, BulkMetric metric, BulkControl control) { + public MockTransportBulkClient maxActionsPerRequest(int maxActions) { return this; } @Override - public MockTransportClient maxActionsPerRequest(int maxActions) { + public MockTransportBulkClient maxConcurrentRequests(int maxConcurrentRequests) { return this; } @Override - public MockTransportClient maxConcurrentRequests(int maxConcurrentRequests) { + public MockTransportBulkClient maxVolumePerRequest(ByteSizeValue maxVolumePerRequest) { return this; } @Override - public MockTransportClient maxVolumePerRequest(ByteSizeValue maxVolumePerRequest) { + public MockTransportBulkClient flushIngestInterval(TimeValue interval) { return this; } @Override - public MockTransportClient flushIngestInterval(TimeValue interval) { + public MockTransportBulkClient index(String index, String type, String id, boolean create, String source) { return this; } @Override - public MockTransportClient index(String index, String type, String id, String source) { + public MockTransportBulkClient delete(String index, String type, String id) { return this; } @Override - public MockTransportClient delete(String index, String type, String id) { + public MockTransportBulkClient update(String index, String type, String id, String source) { return this; } @Override - public MockTransportClient update(String index, String type, String id, String source) { + public MockTransportBulkClient indexRequest(IndexRequest indexRequest) { return this; } @Override - public MockTransportClient bulkIndex(IndexRequest indexRequest) { + public MockTransportBulkClient deleteRequest(DeleteRequest deleteRequest) { return this; } @Override - public MockTransportClient bulkDelete(DeleteRequest deleteRequest) { + public MockTransportBulkClient updateRequest(UpdateRequest updateRequest) { return this; } @Override - public MockTransportClient bulkUpdate(UpdateRequest updateRequest) { + public MockTransportBulkClient flushIngest() { return this; } @Override - public MockTransportClient flushIngest() { + public MockTransportBulkClient waitForResponses(TimeValue timeValue) throws InterruptedException { return this; } @Override - public MockTransportClient waitForResponses(TimeValue timeValue) throws InterruptedException { + public MockTransportBulkClient startBulk(String index, long startRefreshInterval, long stopRefreshIterval) { return this; } @Override - public MockTransportClient startBulk(String index, long startRefreshInterval, long stopRefreshIterval) { + public MockTransportBulkClient stopBulk(String index) { return this; } @Override - public MockTransportClient stopBulk(String index) { + public MockTransportBulkClient deleteIndex(String index) { return this; } @Override - public MockTransportClient deleteIndex(String index) { + public MockTransportBulkClient newIndex(String index) { return this; } @Override - public MockTransportClient newIndex(String index) { - return this; - } - - @Override - public MockTransportClient newMapping(String index, String type, Map mapping) { + public MockTransportBulkClient newMapping(String index, String type, Map mapping) { return this; } @@ -154,5 +148,4 @@ public class MockTransportClient extends BulkTransportClient { public void shutdown() { // mockup method } - } diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4InternalESLogger.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4InternalESLogger.java new file mode 100644 index 0000000..33429bf --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4InternalESLogger.java @@ -0,0 +1,168 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.util.internal.logging.AbstractInternalLogger; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.SuppressLoggerChecks; +import org.elasticsearch.common.logging.Loggers; + +@SuppressLoggerChecks(reason = "safely delegates to logger") +class Netty4InternalESLogger extends AbstractInternalLogger { + + private final Logger logger; + + Netty4InternalESLogger(final String name) { + super(name); + this.logger = Loggers.getLogger(name); + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logger.trace(msg); + } + + @Override + public void trace(String format, Object arg) { + logger.trace(format, arg); + } + + @Override + public void trace(String format, Object argA, Object argB) { + logger.trace(format, argA, argB); + } + + @Override + public void trace(String format, Object... arguments) { + logger.trace(format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logger.trace(msg, t); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logger.debug(msg); + } + + @Override + public void debug(String format, Object arg) { + logger.debug(format, arg); + } + + @Override + public void debug(String format, Object argA, Object argB) { + logger.debug(format, argA, argB); + } + + @Override + public void debug(String format, Object... arguments) { + logger.debug(format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logger.debug(msg, t); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logger.info(msg); + } + + @Override + public void info(String format, Object arg) { + logger.info(format, arg); + } + + @Override + public void info(String format, Object argA, Object argB) { + logger.info(format, argA, argB); + } + + @Override + public void info(String format, Object... arguments) { + logger.info(format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logger.info(msg, t); + } + + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logger.warn(msg); + } + + @Override + public void warn(String format, Object arg) { + logger.warn(format, arg); + } + + @Override + public void warn(String format, Object... arguments) { + logger.warn(format, arguments); + } + + @Override + public void warn(String format, Object argA, Object argB) { + logger.warn(format, argA, argB); + } + + @Override + public void warn(String msg, Throwable t) { + logger.warn(msg, t); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logger.error(msg); + } + + @Override + public void error(String format, Object arg) { + logger.error(format, arg); + } + + @Override + public void error(String format, Object argA, Object argB) { + logger.error(format, argA, argB); + } + + @Override + public void error(String format, Object... arguments) { + logger.error(format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(msg, t); + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4MessageChannelHandler.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4MessageChannelHandler.java new file mode 100644 index 0000000..4126944 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4MessageChannelHandler.java @@ -0,0 +1,58 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.Attribute; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.transport.TcpHeader; +import org.elasticsearch.transport.Transports; + +import java.net.InetSocketAddress; + +/** + * A handler (must be the last one!) that does size based frame decoding and forwards the actual message + * to the relevant action. + */ +final class Netty4MessageChannelHandler extends ChannelDuplexHandler { + + private final Netty4Transport transport; + private final String profileName; + + Netty4MessageChannelHandler(Netty4Transport transport, String profileName) { + this.transport = transport; + this.profileName = profileName; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Transports.assertTransportThread(); + if (!(msg instanceof ByteBuf)) { + ctx.fireChannelRead(msg); + return; + } + final ByteBuf buffer = (ByteBuf) msg; + final int remainingMessageSize = buffer.getInt(buffer.readerIndex() - TcpHeader.MESSAGE_LENGTH_SIZE); + final int expectedReaderIndex = buffer.readerIndex() + remainingMessageSize; + try { + Channel channel = ctx.channel(); + InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); + // netty always copies a buffer, either in NioWorker in its read handler, where it copies to a fresh + // buffer, or in the cumulative buffer, which is cleaned each time so it could be bigger than the actual size + BytesReference reference = Netty4Utils.toBytesReference(buffer, remainingMessageSize); + Attribute channelAttribute = channel.attr(Netty4Transport.CHANNEL_KEY); + transport.messageReceived(reference, channelAttribute.get(), profileName, remoteAddress, remainingMessageSize); + } finally { + // Set the expected position of the buffer, no matter what happened + buffer.readerIndex(expectedReaderIndex); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Netty4Utils.maybeDie(cause); + transport.exceptionCaught(ctx, cause); + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Plugin.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Plugin.java new file mode 100644 index 0000000..cb0b450 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Plugin.java @@ -0,0 +1,77 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.netty4.Netty4HttpServerTransport; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.threadpool.ThreadPool; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class Netty4Plugin extends Plugin implements NetworkPlugin { + + static { + Netty4Utils.setup(); + } + + public static final String NETTY_TRANSPORT_NAME = "netty4"; + public static final String NETTY_HTTP_TRANSPORT_NAME = "netty4"; + + @Override + public List> getSettings() { + return Arrays.asList( + Netty4HttpServerTransport.SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS, + Netty4HttpServerTransport.SETTING_HTTP_WORKER_COUNT, + Netty4HttpServerTransport.SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE, + Netty4HttpServerTransport.SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_MIN, + Netty4HttpServerTransport.SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_MAX, + Netty4Transport.WORKER_COUNT, + Netty4Transport.NETTY_RECEIVE_PREDICTOR_SIZE, + Netty4Transport.NETTY_RECEIVE_PREDICTOR_MIN, + Netty4Transport.NETTY_RECEIVE_PREDICTOR_MAX, + Netty4Transport.NETTY_BOSS_COUNT + ); + } + + @Override + public Settings additionalSettings() { + return Settings.builder() + // here we set the netty4 transport and http transport as the default. This is a set once setting + // ie. if another plugin does that as well the server will fail - only one default network can exist! + .put(NetworkModule.HTTP_DEFAULT_TYPE_SETTING.getKey(), NETTY_HTTP_TRANSPORT_NAME) + .put(NetworkModule.TRANSPORT_DEFAULT_TYPE_SETTING.getKey(), NETTY_TRANSPORT_NAME) + .build(); + } + + @Override + public Map> getTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NetworkService networkService) { + return Collections.singletonMap(NETTY_TRANSPORT_NAME, () -> new Netty4Transport(settings, threadPool, networkService, bigArrays, + namedWriteableRegistry, circuitBreakerService)); + } + + @Override + public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NamedXContentRegistry xContentRegistry, + NetworkService networkService, + HttpServerTransport.Dispatcher dispatcher) { + return Collections.singletonMap(NETTY_HTTP_TRANSPORT_NAME, + () -> new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher)); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4SizeHeaderFrameDecoder.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4SizeHeaderFrameDecoder.java new file mode 100644 index 0000000..bf18d0f --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4SizeHeaderFrameDecoder.java @@ -0,0 +1,30 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.TooLongFrameException; +import org.elasticsearch.transport.TcpHeader; + +import java.util.List; + +final class Netty4SizeHeaderFrameDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + try { + boolean continueProcessing = TcpTransport.validateMessageHeader(Netty4Utils.toBytesReference(in)); + final ByteBuf message = in.skipBytes(TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE); + if (!continueProcessing) return; + out.add(message); + } catch (IllegalArgumentException ex) { + throw new TooLongFrameException(ex); + } catch (IllegalStateException ex) { + /* decode will be called until the ByteBuf is fully consumed; when it is fully + * consumed, transport#validateMessageHeader will throw an IllegalStateException which + * is okay, it means we have finished consuming the ByteBuf and we can get out + */ + } + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Transport.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Transport.java new file mode 100644 index 0000000..6e7df8b --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Transport.java @@ -0,0 +1,339 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.AdaptiveRecvByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.lease.Releasables; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequestOptions; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import static org.elasticsearch.common.settings.Setting.byteSizeSetting; +import static org.elasticsearch.common.settings.Setting.intSetting; +import static org.elasticsearch.common.util.concurrent.ConcurrentCollections.newConcurrentMap; +import static org.elasticsearch.common.util.concurrent.EsExecutors.daemonThreadFactory; + +/** + * There are 4 types of connections per node, low/med/high/ping. Low if for batch oriented APIs (like recovery or + * batch) with high payload that will cause regular request. (like search or single index) to take + * longer. Med is for the typical search / single doc index. And High for things like cluster state. Ping is reserved for + * sending out ping requests to other nodes. + */ +public class Netty4Transport extends TcpTransport { + + static { + Netty4Utils.setup(); + } + + public static final Setting WORKER_COUNT = + new Setting<>("transport.netty.worker_count", + (s) -> Integer.toString(EsExecutors.numberOfProcessors(s) * 2), + (s) -> Setting.parseInt(s, 1, "transport.netty.worker_count"), Property.NodeScope); + + public static final Setting NETTY_RECEIVE_PREDICTOR_SIZE = Setting.byteSizeSetting( + "transport.netty.receive_predictor_size", new ByteSizeValue(64, ByteSizeUnit.KB), Property.NodeScope); + public static final Setting NETTY_RECEIVE_PREDICTOR_MIN = + byteSizeSetting("transport.netty.receive_predictor_min", NETTY_RECEIVE_PREDICTOR_SIZE, Property.NodeScope); + public static final Setting NETTY_RECEIVE_PREDICTOR_MAX = + byteSizeSetting("transport.netty.receive_predictor_max", NETTY_RECEIVE_PREDICTOR_SIZE, Property.NodeScope); + public static final Setting NETTY_BOSS_COUNT = + intSetting("transport.netty.boss_count", 1, 1, Property.NodeScope); + + + protected final RecvByteBufAllocator recvByteBufAllocator; + protected final int workerCount; + protected final ByteSizeValue receivePredictorMin; + protected final ByteSizeValue receivePredictorMax; + protected volatile Bootstrap bootstrap; + protected final Map serverBootstraps = newConcurrentMap(); + + public Netty4Transport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, + NamedWriteableRegistry namedWriteableRegistry, CircuitBreakerService circuitBreakerService) { + super("netty", settings, threadPool, bigArrays, circuitBreakerService, namedWriteableRegistry, networkService); + Netty4Utils.setAvailableProcessors(EsExecutors.PROCESSORS_SETTING.get(settings)); + this.workerCount = WORKER_COUNT.get(settings); + + // See AdaptiveReceiveBufferSizePredictor#DEFAULT_XXX for default values in netty..., we can use higher ones for us, even fixed one + this.receivePredictorMin = NETTY_RECEIVE_PREDICTOR_MIN.get(settings); + this.receivePredictorMax = NETTY_RECEIVE_PREDICTOR_MAX.get(settings); + if (receivePredictorMax.getBytes() == receivePredictorMin.getBytes()) { + recvByteBufAllocator = new FixedRecvByteBufAllocator((int) receivePredictorMax.getBytes()); + } else { + recvByteBufAllocator = new AdaptiveRecvByteBufAllocator((int) receivePredictorMin.getBytes(), + (int) receivePredictorMin.getBytes(), (int) receivePredictorMax.getBytes()); + } + } + + @Override + protected void doStart() { + boolean success = false; + try { + bootstrap = createBootstrap(); + if (NetworkService.NETWORK_SERVER.get(settings)) { + for (ProfileSettings profileSettings : profileSettings) { + createServerBootstrap(profileSettings); + bindServer(profileSettings); + } + } + super.doStart(); + success = true; + } finally { + if (success == false) { + doStop(); + } + } + } + + private Bootstrap createBootstrap() { + final Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(new NioEventLoopGroup(workerCount, daemonThreadFactory(settings, TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX))); + bootstrap.channel(NioSocketChannel.class); + + bootstrap.handler(getClientChannelInitializer()); + + bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.toIntExact(defaultConnectionProfile.getConnectTimeout().millis())); + bootstrap.option(ChannelOption.TCP_NODELAY, TCP_NO_DELAY.get(settings)); + bootstrap.option(ChannelOption.SO_KEEPALIVE, TCP_KEEP_ALIVE.get(settings)); + + final ByteSizeValue tcpSendBufferSize = TCP_SEND_BUFFER_SIZE.get(settings); + if (tcpSendBufferSize.getBytes() > 0) { + bootstrap.option(ChannelOption.SO_SNDBUF, Math.toIntExact(tcpSendBufferSize.getBytes())); + } + + final ByteSizeValue tcpReceiveBufferSize = TCP_RECEIVE_BUFFER_SIZE.get(settings); + if (tcpReceiveBufferSize.getBytes() > 0) { + bootstrap.option(ChannelOption.SO_RCVBUF, Math.toIntExact(tcpReceiveBufferSize.getBytes())); + } + + bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator); + + final boolean reuseAddress = TCP_REUSE_ADDRESS.get(settings); + bootstrap.option(ChannelOption.SO_REUSEADDR, reuseAddress); + + bootstrap.validate(); + + return bootstrap; + } + + private void createServerBootstrap(ProfileSettings profileSettings) { + String name = profileSettings.profileName; + if (logger.isDebugEnabled()) { + logger.debug("using profile[{}], worker_count[{}], port[{}], bind_host[{}], publish_host[{}], compress[{}], " + + "connect_timeout[{}], connections_per_node[{}/{}/{}/{}/{}], receive_predictor[{}->{}]", + name, workerCount, profileSettings.portOrRange, profileSettings.bindHosts, profileSettings.publishHosts, compress, + defaultConnectionProfile.getConnectTimeout(), + defaultConnectionProfile.getNumConnectionsPerType(TransportRequestOptions.Type.RECOVERY), + defaultConnectionProfile.getNumConnectionsPerType(TransportRequestOptions.Type.BULK), + defaultConnectionProfile.getNumConnectionsPerType(TransportRequestOptions.Type.REG), + defaultConnectionProfile.getNumConnectionsPerType(TransportRequestOptions.Type.STATE), + defaultConnectionProfile.getNumConnectionsPerType(TransportRequestOptions.Type.PING), + receivePredictorMin, receivePredictorMax); + } + + + final ThreadFactory workerFactory = daemonThreadFactory(this.settings, TRANSPORT_SERVER_WORKER_THREAD_NAME_PREFIX, name); + + final ServerBootstrap serverBootstrap = new ServerBootstrap(); + + serverBootstrap.group(new NioEventLoopGroup(workerCount, workerFactory)); + serverBootstrap.channel(NioServerSocketChannel.class); + + serverBootstrap.childHandler(getServerChannelInitializer(name)); + + serverBootstrap.childOption(ChannelOption.TCP_NODELAY, profileSettings.tcpNoDelay); + serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, profileSettings.tcpKeepAlive); + + if (profileSettings.sendBufferSize.getBytes() != -1) { + serverBootstrap.childOption(ChannelOption.SO_SNDBUF, Math.toIntExact(profileSettings.sendBufferSize.getBytes())); + } + + if (profileSettings.receiveBufferSize.getBytes() != -1) { + serverBootstrap.childOption(ChannelOption.SO_RCVBUF, Math.toIntExact(profileSettings.receiveBufferSize.bytesAsInt())); + } + + serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator); + serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, recvByteBufAllocator); + + serverBootstrap.option(ChannelOption.SO_REUSEADDR, profileSettings.reuseAddress); + serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, profileSettings.reuseAddress); + serverBootstrap.validate(); + + serverBootstraps.put(name, serverBootstrap); + } + + protected ChannelHandler getServerChannelInitializer(String name) { + return new ServerChannelInitializer(name); + } + + protected ChannelHandler getClientChannelInitializer() { + return new ClientChannelInitializer(); + } + + static final AttributeKey CHANNEL_KEY = AttributeKey.newInstance("es-channel-client"); + + protected final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + final Throwable unwrapped = ExceptionsHelper.unwrap(cause, ElasticsearchException.class); + final Throwable t = unwrapped != null ? unwrapped : cause; + Channel channel = ctx.channel(); + onException(channel.attr(CHANNEL_KEY).get(), t instanceof Exception ? (Exception) t : new ElasticsearchException(t)); + } + + @Override + protected NettyTcpChannel initiateChannel(DiscoveryNode node, TimeValue connectTimeout, ActionListener listener) + throws IOException { + ChannelFuture channelFuture = bootstrap.connect(node.getAddress().address()); + Channel channel = channelFuture.channel(); + if (channel == null) { + Netty4Utils.maybeDie(channelFuture.cause()); + throw new IOException(channelFuture.cause()); + } + addClosedExceptionLogger(channel); + + NettyTcpChannel nettyChannel = new NettyTcpChannel(channel); + channel.attr(CHANNEL_KEY).set(nettyChannel); + + channelFuture.addListener(f -> { + if (f.isSuccess()) { + listener.onResponse(null); + } else { + Throwable cause = f.cause(); + if (cause instanceof Error) { + Netty4Utils.maybeDie(cause); + listener.onFailure(new Exception(cause)); + } else { + listener.onFailure((Exception) cause); + } + } + }); + + return nettyChannel; + } + + @Override + protected NettyTcpChannel bind(String name, InetSocketAddress address) { + Channel channel = serverBootstraps.get(name).bind(address).syncUninterruptibly().channel(); + NettyTcpChannel esChannel = new NettyTcpChannel(channel); + channel.attr(CHANNEL_KEY).set(esChannel); + return esChannel; + } + + ScheduledPing getPing() { + return scheduledPing; + } + + @Override + @SuppressForbidden(reason = "debug") + protected void stopInternal() { + Releasables.close(() -> { + final List>> serverBootstrapCloseFutures = new ArrayList<>(serverBootstraps.size()); + for (final Map.Entry entry : serverBootstraps.entrySet()) { + serverBootstrapCloseFutures.add( + Tuple.tuple(entry.getKey(), entry.getValue().config().group().shutdownGracefully(0, 5, TimeUnit.SECONDS))); + } + for (final Tuple> future : serverBootstrapCloseFutures) { + future.v2().awaitUninterruptibly(); + if (!future.v2().isSuccess()) { + logger.debug( + (Supplier) () -> new ParameterizedMessage( + "Error closing server bootstrap for profile [{}]", future.v1()), future.v2().cause()); + } + } + serverBootstraps.clear(); + + if (bootstrap != null) { + bootstrap.config().group().shutdownGracefully(0, 5, TimeUnit.SECONDS).awaitUninterruptibly(); + bootstrap = null; + } + }); + } + + protected class ClientChannelInitializer extends ChannelInitializer { + + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast("logging", new ESLoggingHandler()); + ch.pipeline().addLast("size", new Netty4SizeHeaderFrameDecoder()); + // using a dot as a prefix means this cannot come from any settings parsed + ch.pipeline().addLast("dispatcher", new Netty4MessageChannelHandler(Netty4Transport.this, ".client")); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Netty4Utils.maybeDie(cause); + super.exceptionCaught(ctx, cause); + } + } + + protected class ServerChannelInitializer extends ChannelInitializer { + + protected final String name; + + protected ServerChannelInitializer(String name) { + this.name = name; + } + + @Override + protected void initChannel(Channel ch) throws Exception { + addClosedExceptionLogger(ch); + NettyTcpChannel nettyTcpChannel = new NettyTcpChannel(ch); + ch.attr(CHANNEL_KEY).set(nettyTcpChannel); + serverAcceptedChannel(nettyTcpChannel); + ch.pipeline().addLast("logging", new ESLoggingHandler()); + ch.pipeline().addLast("size", new Netty4SizeHeaderFrameDecoder()); + ch.pipeline().addLast("dispatcher", new Netty4MessageChannelHandler(Netty4Transport.this, name)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Netty4Utils.maybeDie(cause); + super.exceptionCaught(ctx, cause); + } + } + + private void addClosedExceptionLogger(Channel channel) { + channel.closeFuture().addListener(f -> { + if (f.isSuccess() == false) { + logger.debug(() -> new ParameterizedMessage("exception while closing channel: {}", channel), f.cause()); + } + }); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Utils.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Utils.java new file mode 100644 index 0000000..355f4c9 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Netty4Utils.java @@ -0,0 +1,164 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.util.NettyRuntime; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefIterator; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.logging.ESLoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class Netty4Utils { + + static { + InternalLoggerFactory.setDefaultFactory(new InternalLoggerFactory() { + + @Override + public InternalLogger newInstance(final String name) { + return new Netty4InternalESLogger(name); + } + + }); + } + + public static void setup() { + + } + + private static AtomicBoolean isAvailableProcessorsSet = new AtomicBoolean(); + + /** + * Set the number of available processors that Netty uses for sizing various resources (e.g., thread pools). + * + * @param availableProcessors the number of available processors + * @throws IllegalStateException if available processors was set previously and the specified value does not match the already-set value + */ + public static void setAvailableProcessors(final int availableProcessors) { + // we set this to false in tests to avoid tests that randomly set processors from stepping on each other + final boolean set = Booleans.parseBoolean(System.getProperty("es.set.netty.runtime.available.processors", "true")); + if (!set) { + return; + } + + try { + NettyRuntime.setAvailableProcessors(availableProcessors); + } catch (IllegalStateException e) { + // + } + } + + /** + * Turns the given BytesReference into a ByteBuf. Note: the returned ByteBuf will reference the internal + * pages of the BytesReference. Don't free the bytes of reference before the ByteBuf goes out of scope. + */ + public static ByteBuf toByteBuf(final BytesReference reference) { + if (reference.length() == 0) { + return Unpooled.EMPTY_BUFFER; + } + if (reference instanceof ByteBufBytesReference) { + return ((ByteBufBytesReference) reference).toByteBuf(); + } else { + final BytesRefIterator iterator = reference.iterator(); + // usually we have one, two, or three components from the header, the message, and a buffer + final List buffers = new ArrayList<>(3); + try { + BytesRef slice; + while ((slice = iterator.next()) != null) { + buffers.add(Unpooled.wrappedBuffer(slice.bytes, slice.offset, slice.length)); + } + final CompositeByteBuf composite = Unpooled.compositeBuffer(buffers.size()); + composite.addComponents(true, buffers); + return composite; + } catch (IOException ex) { + throw new AssertionError("no IO happens here", ex); + } + } + } + + /** + * Wraps the given ChannelBuffer with a BytesReference + */ + public static BytesReference toBytesReference(final ByteBuf buffer) { + return toBytesReference(buffer, buffer.readableBytes()); + } + + /** + * Wraps the given ChannelBuffer with a BytesReference of a given size + */ + static BytesReference toBytesReference(final ByteBuf buffer, final int size) { + return new ByteBufBytesReference(buffer, size); + } + + public static void closeChannels(final Collection channels) throws IOException { + IOException closingExceptions = null; + final List futures = new ArrayList<>(); + for (final Channel channel : channels) { + try { + if (channel != null && channel.isOpen()) { + futures.add(channel.close()); + } + } catch (Exception e) { + if (closingExceptions == null) { + closingExceptions = new IOException("failed to close channels"); + } + closingExceptions.addSuppressed(e); + } + } + for (final ChannelFuture future : futures) { + future.awaitUninterruptibly(); + } + + if (closingExceptions != null) { + throw closingExceptions; + } + } + + /** + * If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be + * caught and bubbles up to the uncaught exception handler. + * + * @param cause the throwable to test + */ + public static void maybeDie(final Throwable cause) { + final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class); + final Optional maybeError = ExceptionsHelper.maybeError(cause, logger); + if (maybeError.isPresent()) { + /* + * Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, Netty wraps too many + * invocations of user-code in try/catch blocks that swallow all throwables. This means that a rethrow here will not bubble up + * to where we want it to. So, we fork a thread and throw the exception from there where Netty can not get to it. We do not wrap + * the exception so as to not lose the original cause during exit. + */ + try { + // try to log the current stack trace + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + final String formatted = Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n")); + logger.error("fatal error on the network layer\n{}", formatted); + } finally { + new Thread( + () -> { + throw maybeError.get(); + }) + .start(); + } + } + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/NettyTcpChannel.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NettyTcpChannel.java new file mode 100644 index 0000000..b0e5f73 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NettyTcpChannel.java @@ -0,0 +1,92 @@ +package org.xbib.elasticsearch.client.transport; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPromise; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.transport.TcpChannel; +import org.elasticsearch.transport.TransportException; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +public class NettyTcpChannel implements TcpChannel { + + private final Channel channel; + private final CompletableFuture closeContext = new CompletableFuture<>(); + + NettyTcpChannel(Channel channel) { + this.channel = channel; + this.channel.closeFuture().addListener(f -> { + if (f.isSuccess()) { + closeContext.complete(null); + } else { + Throwable cause = f.cause(); + if (cause instanceof Error) { + Netty4Utils.maybeDie(cause); + closeContext.completeExceptionally(cause); + } else { + closeContext.completeExceptionally(cause); + } + } + }); + } + + @Override + public void close() { + channel.close(); + } + + @Override + public void addCloseListener(ActionListener listener) { + closeContext.whenComplete(ActionListener.toBiConsumer(listener)); + } + + @Override + public void setSoLinger(int value) { + channel.config().setOption(ChannelOption.SO_LINGER, value); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return (InetSocketAddress) channel.localAddress(); + } + + @Override + public void sendMessage(BytesReference reference, ActionListener listener) { + ChannelPromise writePromise = channel.newPromise(); + writePromise.addListener(f -> { + if (f.isSuccess()) { + listener.onResponse(null); + } else { + final Throwable cause = f.cause(); + Netty4Utils.maybeDie(cause); + assert cause instanceof Exception; + listener.onFailure((Exception) cause); + } + }); + channel.writeAndFlush(Netty4Utils.toByteBuf(reference), writePromise); + + if (channel.eventLoop().isShutdown()) { + listener.onFailure(new TransportException("Cannot send message, event loop is shutting down.")); + } + } + + public Channel getLowLevelChannel() { + return channel; + } + + @Override + public String toString() { + return "NettyTcpChannel{" + + "localAddress=" + getLocalAddress() + + ", remoteAddress=" + channel.remoteAddress() + + '}'; + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkModule.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkModule.java new file mode 100644 index 0000000..820353e --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkModule.java @@ -0,0 +1,237 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.action.support.replication.ReplicationTask; +import org.elasticsearch.cluster.routing.allocation.command.AllocateEmptyPrimaryAllocationCommand; +import org.elasticsearch.cluster.routing.allocation.command.AllocateReplicaAllocationCommand; +import org.elasticsearch.cluster.routing.allocation.command.AllocateStalePrimaryAllocationCommand; +import org.elasticsearch.cluster.routing.allocation.command.AllocationCommand; +import org.elasticsearch.cluster.routing.allocation.command.CancelAllocationCommand; +import org.elasticsearch.cluster.routing.allocation.command.MoveAllocationCommand; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.tasks.RawTaskStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * A module to handle registering and binding all network related classes. + */ +public final class NetworkModule { + + public static final String TRANSPORT_TYPE_KEY = "transport.type"; + public static final String HTTP_TYPE_KEY = "http.type"; + public static final String HTTP_TYPE_DEFAULT_KEY = "http.type.default"; + public static final String TRANSPORT_TYPE_DEFAULT_KEY = "transport.type.default"; + + public static final Setting TRANSPORT_DEFAULT_TYPE_SETTING = Setting.simpleString(TRANSPORT_TYPE_DEFAULT_KEY, + Property.NodeScope); + public static final Setting HTTP_DEFAULT_TYPE_SETTING = Setting.simpleString(HTTP_TYPE_DEFAULT_KEY, Property.NodeScope); + public static final Setting HTTP_TYPE_SETTING = Setting.simpleString(HTTP_TYPE_KEY, Property.NodeScope); + public static final Setting HTTP_ENABLED = Setting.boolSetting("http.enabled", true, Property.NodeScope); + public static final Setting TRANSPORT_TYPE_SETTING = Setting.simpleString(TRANSPORT_TYPE_KEY, Property.NodeScope); + + private final Settings settings; + private final boolean transportClient; + + private static final List namedWriteables = new ArrayList<>(); + private static final List namedXContents = new ArrayList<>(); + + static { + registerAllocationCommand(CancelAllocationCommand::new, CancelAllocationCommand::fromXContent, + CancelAllocationCommand.COMMAND_NAME_FIELD); + registerAllocationCommand(MoveAllocationCommand::new, MoveAllocationCommand::fromXContent, + MoveAllocationCommand.COMMAND_NAME_FIELD); + registerAllocationCommand(AllocateReplicaAllocationCommand::new, AllocateReplicaAllocationCommand::fromXContent, + AllocateReplicaAllocationCommand.COMMAND_NAME_FIELD); + registerAllocationCommand(AllocateEmptyPrimaryAllocationCommand::new, AllocateEmptyPrimaryAllocationCommand::fromXContent, + AllocateEmptyPrimaryAllocationCommand.COMMAND_NAME_FIELD); + registerAllocationCommand(AllocateStalePrimaryAllocationCommand::new, AllocateStalePrimaryAllocationCommand::fromXContent, + AllocateStalePrimaryAllocationCommand.COMMAND_NAME_FIELD); + namedWriteables.add( + new NamedWriteableRegistry.Entry(Task.Status.class, ReplicationTask.Status.NAME, ReplicationTask.Status::new)); + namedWriteables.add( + new NamedWriteableRegistry.Entry(Task.Status.class, RawTaskStatus.NAME, RawTaskStatus::new)); + } + + private final Map> transportFactories = new HashMap<>(); + private final Map> transportHttpFactories = new HashMap<>(); + private final List transportIntercetors = new ArrayList<>(); + + /** + * Creates a network module that custom networking classes can be plugged into. + * @param settings The settings for the node + * @param transportClient True if only transport classes should be allowed to be registered, false otherwise. + */ + public NetworkModule(Settings settings, boolean transportClient, List plugins, ThreadPool threadPool, + BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NamedXContentRegistry xContentRegistry, + NetworkService networkService, HttpServerTransport.Dispatcher dispatcher) { + this.settings = settings; + this.transportClient = transportClient; + for (NetworkPlugin plugin : plugins) { + if (transportClient == false && HTTP_ENABLED.get(settings)) { + Map> httpTransportFactory = plugin.getHttpTransports(settings, threadPool, bigArrays, + circuitBreakerService, namedWriteableRegistry, xContentRegistry, networkService, dispatcher); + for (Map.Entry> entry : httpTransportFactory.entrySet()) { + registerHttpTransport(entry.getKey(), entry.getValue()); + } + } + Map> transportFactory = plugin.getTransports(settings, threadPool, bigArrays, pageCacheRecycler, + circuitBreakerService, namedWriteableRegistry, networkService); + for (Map.Entry> entry : transportFactory.entrySet()) { + registerTransport(entry.getKey(), entry.getValue()); + } + List transportInterceptors = plugin.getTransportInterceptors(namedWriteableRegistry, + threadPool.getThreadContext()); + for (TransportInterceptor interceptor : transportInterceptors) { + registerTransportInterceptor(interceptor); + } + } + } + + public boolean isTransportClient() { + return transportClient; + } + + /** Adds a transport implementation that can be selected by setting {@link #TRANSPORT_TYPE_KEY}. */ + private void registerTransport(String key, Supplier factory) { + if (transportFactories.putIfAbsent(key, factory) != null) { + throw new IllegalArgumentException("transport for name: " + key + " is already registered"); + } + } + + /** Adds an http transport implementation that can be selected by setting {@link #HTTP_TYPE_KEY}. */ + // TODO: we need another name than "http transport"....so confusing with transportClient... + private void registerHttpTransport(String key, Supplier factory) { + if (transportClient) { + throw new IllegalArgumentException("Cannot register http transport " + key + " for transport client"); + } + if (transportHttpFactories.putIfAbsent(key, factory) != null) { + throw new IllegalArgumentException("transport for name: " + key + " is already registered"); + } + } + + /** + * Register an allocation command. + *

+ * This lives here instead of the more aptly named ClusterModule because the Transport client needs these to be registered. + *

+ * @param reader the reader to read it from a stream + * @param parser the parser to read it from XContent + * @param commandName the names under which the command should be parsed. The {@link ParseField#getPreferredName()} is special because + * it is the name under which the command's reader is registered. + */ + private static void registerAllocationCommand(Writeable.Reader reader, + CheckedFunction parser, ParseField commandName) { + namedXContents.add(new NamedXContentRegistry.Entry(AllocationCommand.class, commandName, parser)); + namedWriteables.add(new NamedWriteableRegistry.Entry(AllocationCommand.class, commandName.getPreferredName(), reader)); + } + + public static List getNamedWriteables() { + return Collections.unmodifiableList(namedWriteables); + } + + public static List getNamedXContents() { + return Collections.unmodifiableList(namedXContents); + } + + public Supplier getHttpServerTransportSupplier() { + final String name; + if (HTTP_TYPE_SETTING.exists(settings)) { + name = HTTP_TYPE_SETTING.get(settings); + } else { + name = HTTP_DEFAULT_TYPE_SETTING.get(settings); + } + final Supplier factory = transportHttpFactories.get(name); + if (factory == null) { + throw new IllegalStateException("Unsupported http.type [" + name + "]"); + } + return factory; + } + + public boolean isHttpEnabled() { + return transportClient == false && HTTP_ENABLED.get(settings); + } + + public Supplier getTransportSupplier() { + final String name; + if (TRANSPORT_TYPE_SETTING.exists(settings)) { + name = TRANSPORT_TYPE_SETTING.get(settings); + } else { + name = TRANSPORT_DEFAULT_TYPE_SETTING.get(settings); + } + final Supplier factory = transportFactories.get(name); + if (factory == null) { + throw new IllegalStateException("Unsupported transport.type [" + name + "] factories = " + transportFactories); + } + return factory; + } + + /** + * Registers a new {@link TransportInterceptor} + */ + private void registerTransportInterceptor(TransportInterceptor interceptor) { + this.transportIntercetors.add(Objects.requireNonNull(interceptor, "interceptor must not be null")); + } + + /** + * Returns a composite {@link TransportInterceptor} containing all registered interceptors + * @see #registerTransportInterceptor(TransportInterceptor) + */ + public TransportInterceptor getTransportInterceptor() { + return new CompositeTransportInterceptor(this.transportIntercetors); + } + + static final class CompositeTransportInterceptor implements TransportInterceptor { + final List transportInterceptors; + + private CompositeTransportInterceptor(List transportInterceptors) { + this.transportInterceptors = new ArrayList<>(transportInterceptors); + } + + @Override + public TransportRequestHandler interceptHandler(String action, String executor, + boolean forceExecution, + TransportRequestHandler actualHandler) { + for (TransportInterceptor interceptor : this.transportInterceptors) { + actualHandler = interceptor.interceptHandler(action, executor, forceExecution, actualHandler); + } + return actualHandler; + } + + @Override + public AsyncSender interceptSender(AsyncSender sender) { + for (TransportInterceptor interceptor : this.transportInterceptors) { + sender = interceptor.interceptSender(sender); + } + return sender; + } + } + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkPlugin.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkPlugin.java new file mode 100644 index 0000000..f566813 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/NetworkPlugin.java @@ -0,0 +1,61 @@ +package org.xbib.elasticsearch.client.transport; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.threadpool.ThreadPool; + +/** + * Plugin for extending network and transport related classes + */ +public interface NetworkPlugin { + + /** + * Returns a list of {@link TransportInterceptor} instances that are used to intercept incoming and outgoing + * transport (inter-node) requests. This must not return null + * + * @param namedWriteableRegistry registry of all named writeables registered + * @param threadContext a {@link ThreadContext} of the current nodes or clients {@link ThreadPool} that can be used to set additional + * headers in the interceptors + */ + default List getTransportInterceptors(NamedWriteableRegistry namedWriteableRegistry, + ThreadContext threadContext) { + return Collections.emptyList(); + } + + /** + * Returns a map of {@link Transport} suppliers. + * See {@link org.elasticsearch.common.network.NetworkModule#TRANSPORT_TYPE_KEY} to configure a specific implementation. + */ + default Map> getTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NetworkService networkService) { + return Collections.emptyMap(); + } + + /** + * Returns a map of {@link HttpServerTransport} suppliers. + * See {@link org.elasticsearch.common.network.NetworkModule#HTTP_TYPE_SETTING} to configure a specific implementation. + */ + default Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NamedXContentRegistry xContentRegistry, + NetworkService networkService, + HttpServerTransport.Dispatcher dispatcher) { + return Collections.emptyMap(); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterConnection.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterConnection.java new file mode 100644 index 0000000..2251158 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterConnection.java @@ -0,0 +1,728 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; +import org.apache.lucene.store.AlreadyClosedException; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoAction; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsAction; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsResponse; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; +import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.CancellableThreads; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.TransportActionProxy; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponseHandler; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Represents a connection to a single remote cluster. In contrast to a local cluster a remote cluster is not joined such that the + * current node is part of the cluster and it won't receive cluster state updates from the remote cluster. Remote clusters are also not + * fully connected with the current node. From a connection perspective a local cluster forms a bi-directional star network while in the + * remote case we only connect to a subset of the nodes in the cluster in an uni-directional fashion. + * + * This class also handles the discovery of nodes from the remote cluster. The initial list of seed nodes is only used to discover all nodes + * in the remote cluster and connects to all eligible nodes, for details see {@link RemoteClusterService#REMOTE_NODE_ATTRIBUTE}. + * + * In the case of a disconnection, this class will issue a re-connect task to establish at most + * {@link RemoteClusterService#REMOTE_CONNECTIONS_PER_CLUSTER} until either all eligible nodes are exhausted or the maximum number of + * connections per cluster has been reached. + */ +final class RemoteClusterConnection extends AbstractComponent implements TransportConnectionListener, Closeable { + + private final TransportService transportService; + private final ConnectionProfile remoteProfile; + private final ConnectedNodes connectedNodes; + private final String clusterAlias; + private final int maxNumRemoteConnections; + private final Predicate nodePredicate; + private volatile List seedNodes; + private volatile boolean skipUnavailable; + private final ConnectHandler connectHandler; + private SetOnce remoteClusterName = new SetOnce<>(); + + /** + * Creates a new {@link RemoteClusterConnection} + * @param settings the nodes settings object + * @param clusterAlias the configured alias of the cluster to connect to + * @param seedNodes a list of seed nodes to discover eligible nodes from + * @param transportService the local nodes transport service + * @param maxNumRemoteConnections the maximum number of connections to the remote cluster + * @param nodePredicate a predicate to filter eligible remote nodes to connect to + */ + RemoteClusterConnection(Settings settings, String clusterAlias, List seedNodes, + TransportService transportService, int maxNumRemoteConnections, Predicate nodePredicate) { + super(settings); + this.transportService = transportService; + this.maxNumRemoteConnections = maxNumRemoteConnections; + this.nodePredicate = nodePredicate; + this.clusterAlias = clusterAlias; + ConnectionProfile.Builder builder = new ConnectionProfile.Builder(); + builder.setConnectTimeout(TcpTransport.TCP_CONNECT_TIMEOUT.get(settings)); + builder.setHandshakeTimeout(TcpTransport.TCP_CONNECT_TIMEOUT.get(settings)); + builder.addConnections(6, TransportRequestOptions.Type.REG, TransportRequestOptions.Type.PING); // TODO make this configurable? + builder.addConnections(0, // we don't want this to be used for anything else but search + TransportRequestOptions.Type.BULK, + TransportRequestOptions.Type.STATE, + TransportRequestOptions.Type.RECOVERY); + remoteProfile = builder.build(); + connectedNodes = new ConnectedNodes(clusterAlias); + this.seedNodes = Collections.unmodifiableList(seedNodes); + this.skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE + .getConcreteSettingForNamespace(clusterAlias).get(settings); + this.connectHandler = new ConnectHandler(); + transportService.addConnectionListener(this); + } + + /** + * Updates the list of seed nodes for this cluster connection + */ + synchronized void updateSeedNodes(List seedNodes, ActionListener connectListener) { + this.seedNodes = Collections.unmodifiableList(new ArrayList<>(seedNodes)); + connectHandler.connect(connectListener); + } + + /** + * Updates the skipUnavailable flag that can be dynamically set for each remote cluster + */ + void updateSkipUnavailable(boolean skipUnavailable) { + this.skipUnavailable = skipUnavailable; + } + + @Override + public void onNodeDisconnected(DiscoveryNode node) { + boolean remove = connectedNodes.remove(node); + if (remove && connectedNodes.size() < maxNumRemoteConnections) { + // try to reconnect and fill up the slot of the disconnected node + connectHandler.forceConnect(); + } + } + + /** + * Fetches all shards for the search request from this remote connection. This is used to later run the search on the remote end. + */ + public void fetchSearchShards(ClusterSearchShardsRequest searchRequest, + ActionListener listener) { + + final ActionListener searchShardsListener; + final Consumer onConnectFailure; + if (skipUnavailable) { + onConnectFailure = (exception) -> listener.onResponse(ClusterSearchShardsResponse.EMPTY); + searchShardsListener = ActionListener.wrap(listener::onResponse, (e) -> listener.onResponse(ClusterSearchShardsResponse.EMPTY)); + } else { + onConnectFailure = listener::onFailure; + searchShardsListener = listener; + } + // in case we have no connected nodes we try to connect and if we fail we either notify the listener or not depending on + // the skip_unavailable setting + ensureConnected(ActionListener.wrap((x) -> fetchShardsInternal(searchRequest, searchShardsListener), onConnectFailure)); + } + + /** + * Ensures that this cluster is connected. If the cluster is connected this operation + * will invoke the listener immediately. + */ + public void ensureConnected(ActionListener voidActionListener) { + if (connectedNodes.size() == 0) { + connectHandler.connect(voidActionListener); + } else { + voidActionListener.onResponse(null); + } + } + + private void fetchShardsInternal(ClusterSearchShardsRequest searchShardsRequest, + final ActionListener listener) { + final DiscoveryNode node = connectedNodes.get(); + transportService.sendRequest(node, ClusterSearchShardsAction.NAME, searchShardsRequest, + new TransportResponseHandler() { + + @Override + public ClusterSearchShardsResponse newInstance() { + return new ClusterSearchShardsResponse(); + } + + @Override + public void handleResponse(ClusterSearchShardsResponse clusterSearchShardsResponse) { + listener.onResponse(clusterSearchShardsResponse); + } + + @Override + public void handleException(TransportException e) { + listener.onFailure(e); + } + + @Override + public String executor() { + return ThreadPool.Names.SEARCH; + } + }); + } + + /** + * Collects all nodes on the connected cluster and returns / passes a nodeID to {@link DiscoveryNode} lookup function + * that returns null if the node ID is not found. + */ + void collectNodes(ActionListener> listener) { + Runnable runnable = () -> { + final ClusterStateRequest request = new ClusterStateRequest(); + request.clear(); + request.nodes(true); + request.local(true); // run this on the node that gets the request it's as good as any other + final DiscoveryNode node = connectedNodes.get(); + transportService.sendRequest(node, ClusterStateAction.NAME, request, TransportRequestOptions.EMPTY, + new TransportResponseHandler() { + @Override + public ClusterStateResponse newInstance() { + return new ClusterStateResponse(); + } + + @Override + public void handleResponse(ClusterStateResponse response) { + DiscoveryNodes nodes = response.getState().nodes(); + listener.onResponse(nodes::get); + } + + @Override + public void handleException(TransportException exp) { + listener.onFailure(exp); + } + + @Override + public String executor() { + return ThreadPool.Names.SAME; + } + }); + }; + try { + // just in case if we are not connected for some reason we try to connect and if we fail we have to notify the listener + // this will cause some back pressure on the search end and eventually will cause rejections but that's fine + // we can't proceed with a search on a cluster level. + // in the future we might want to just skip the remote nodes in such a case but that can already be implemented on the + // caller end since they provide the listener. + ensureConnected(ActionListener.wrap((x) -> runnable.run(), listener::onFailure)); + } catch (Exception ex) { + listener.onFailure(ex); + } + } + + /** + * Returns a connection to the remote cluster. This connection might be a proxy connection that redirects internally to the + * given node. + */ + Transport.Connection getConnection(DiscoveryNode remoteClusterNode) { + DiscoveryNode discoveryNode = connectedNodes.get(); + Transport.Connection connection = transportService.getConnection(discoveryNode); + return new Transport.Connection() { + @Override + public DiscoveryNode getNode() { + return remoteClusterNode; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + connection.sendRequest(requestId, TransportActionProxy.getProxyAction(action), + TransportActionProxy.wrapRequest(remoteClusterNode, request), options); + } + + @Override + public void close() throws IOException { + assert false: "proxy connections must not be closed"; + } + + @Override + public Version getVersion() { + return connection.getVersion(); + } + }; + } + + Transport.Connection getConnection() { + DiscoveryNode discoveryNode = connectedNodes.get(); + return transportService.getConnection(discoveryNode); + } + + @Override + public void close() throws IOException { + connectHandler.close(); + } + + public boolean isClosed() { + return connectHandler.isClosed(); + } + + /** + * The connect handler manages node discovery and the actual connect to the remote cluster. + * There is at most one connect job running at any time. If such a connect job is triggered + * while another job is running the provided listeners are queued and batched up until the current running job returns. + * + * The handler has a built-in queue that can hold up to 100 connect attempts and will reject requests once the queue is full. + * In a scenario when a remote cluster becomes unavailable we will queue requests up but if we can't connect quick enough + * we will just reject the connect trigger which will lead to failing searches. + */ + private class ConnectHandler implements Closeable { + private final Semaphore running = new Semaphore(1); + private final AtomicBoolean closed = new AtomicBoolean(false); + private final BlockingQueue> queue = new ArrayBlockingQueue<>(100); + private final CancellableThreads cancellableThreads = new CancellableThreads(); + + /** + * Triggers a connect round iff there are pending requests queued up and if there is no + * connect round currently running. + */ + void maybeConnect() { + connect(null); + } + + /** + * Triggers a connect round unless there is one running already. If there is a connect round running, the listener will either + * be queued or rejected and failed. + */ + void connect(ActionListener connectListener) { + connect(connectListener, false); + } + + /** + * Triggers a connect round unless there is one already running. In contrast to {@link #maybeConnect()} will this method also + * trigger a connect round if there is no listener queued up. + */ + void forceConnect() { + connect(null, true); + } + + private void connect(ActionListener connectListener, boolean forceRun) { + final boolean runConnect; + final Collection> toNotify; + synchronized (queue) { + if (connectListener != null && queue.offer(connectListener) == false) { + connectListener.onFailure(new RejectedExecutionException("connect queue is full")); + return; + } + if (forceRun == false && queue.isEmpty()) { + return; + } + runConnect = running.tryAcquire(); + if (runConnect) { + toNotify = new ArrayList<>(); + queue.drainTo(toNotify); + if (closed.get()) { + running.release(); + ActionListener.onFailure(toNotify, new AlreadyClosedException("connect handler is already closed")); + return; + } + } else { + toNotify = Collections.emptyList(); + } + } + if (runConnect) { + forkConnect(toNotify); + } + } + + private void forkConnect(final Collection> toNotify) { + ThreadPool threadPool = transportService.getThreadPool(); + ExecutorService executor = threadPool.executor(ThreadPool.Names.MANAGEMENT); + executor.submit(new AbstractRunnable() { + @Override + public void onFailure(Exception e) { + synchronized (queue) { + running.release(); + } + try { + ActionListener.onFailure(toNotify, e); + } finally { + maybeConnect(); + } + } + + @Override + protected void doRun() throws Exception { + ActionListener listener = ActionListener.wrap((x) -> { + synchronized (queue) { + running.release(); + } + try { + ActionListener.onResponse(toNotify, x); + } finally { + maybeConnect(); + } + + }, (e) -> { + synchronized (queue) { + running.release(); + } + try { + ActionListener.onFailure(toNotify, e); + } finally { + maybeConnect(); + } + }); + collectRemoteNodes(seedNodes.iterator(), transportService, listener); + } + }); + + } + + void collectRemoteNodes(Iterator seedNodes, + final TransportService transportService, ActionListener listener) { + if (Thread.currentThread().isInterrupted()) { + listener.onFailure(new InterruptedException("remote connect thread got interrupted")); + } + try { + if (seedNodes.hasNext()) { + cancellableThreads.executeIO(() -> { + final DiscoveryNode seedNode = seedNodes.next(); + final DiscoveryNode handshakeNode; + Transport.Connection connection = transportService.openConnection(seedNode, + ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, null, null)); + boolean success = false; + try { + try { + handshakeNode = transportService.handshake(connection, remoteProfile.getHandshakeTimeout().millis(), + (c) -> remoteClusterName.get() == null ? true : c.equals(remoteClusterName.get())); + } catch (IllegalStateException ex) { + logger.warn((Supplier) () -> new ParameterizedMessage("seed node {} cluster name mismatch expected " + + "cluster name {}", connection.getNode(), remoteClusterName.get()), ex); + throw ex; + } + if (nodePredicate.test(handshakeNode) && connectedNodes.size() < maxNumRemoteConnections) { + transportService.connectToNode(handshakeNode, remoteProfile); + connectedNodes.add(handshakeNode); + } + ClusterStateRequest request = new ClusterStateRequest(); + request.clear(); + request.nodes(true); + // here we pass on the connection since we can only close it once the sendRequest returns otherwise + // due to the async nature (it will return before it's actually sent) this can cause the request to fail + // due to an already closed connection. + ThreadPool threadPool = transportService.getThreadPool(); + ThreadContext threadContext = threadPool.getThreadContext(); + TransportService.ContextRestoreResponseHandler responseHandler = new TransportService + .ContextRestoreResponseHandler<>(threadContext.newRestorableContext(false), + new SniffClusterStateResponseHandler(transportService, connection, listener, seedNodes, + cancellableThreads)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + // we stash any context here since this is an internal execution and should not leak any + // existing context information. + threadContext.markAsSystemContext(); + transportService.sendRequest(connection, ClusterStateAction.NAME, request, TransportRequestOptions.EMPTY, + responseHandler); + } + success = true; + } finally { + if (success == false) { + connection.close(); + } + } + }); + } else { + listener.onFailure(new IllegalStateException("no seed node left")); + } + } catch (CancellableThreads.ExecutionCancelledException ex) { + listener.onFailure(ex); // we got canceled - fail the listener and step out + } catch (ConnectTransportException | IOException | IllegalStateException ex) { + // ISE if we fail the handshake with an version incompatible node + if (seedNodes.hasNext()) { + logger.debug((Supplier) () -> new ParameterizedMessage("fetching nodes from external cluster {} failed", + clusterAlias), ex); + collectRemoteNodes(seedNodes, transportService, listener); + } else { + listener.onFailure(ex); + } + } + } + + @Override + public void close() throws IOException { + try { + if (closed.compareAndSet(false, true)) { + cancellableThreads.cancel("connect handler is closed"); + running.acquire(); // acquire the semaphore to ensure all connections are closed and all thread joined + running.release(); + maybeConnect(); // now go and notify pending listeners + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + final boolean isClosed() { + return closed.get(); + } + + /* This class handles the _state response from the remote cluster when sniffing nodes to connect to */ + private class SniffClusterStateResponseHandler implements TransportResponseHandler { + + private final TransportService transportService; + private final Transport.Connection connection; + private final ActionListener listener; + private final Iterator seedNodes; + private final CancellableThreads cancellableThreads; + + SniffClusterStateResponseHandler(TransportService transportService, Transport.Connection connection, + ActionListener listener, Iterator seedNodes, + CancellableThreads cancellableThreads) { + this.transportService = transportService; + this.connection = connection; + this.listener = listener; + this.seedNodes = seedNodes; + this.cancellableThreads = cancellableThreads; + } + + @Override + public ClusterStateResponse newInstance() { + return new ClusterStateResponse(); + } + + @Override + public void handleResponse(ClusterStateResponse response) { + assert transportService.getThreadPool().getThreadContext().isSystemContext() == false : "context is a system context"; + try { + if (remoteClusterName.get() == null) { + assert response.getClusterName().value() != null; + remoteClusterName.set(response.getClusterName()); + } + try (Closeable theConnection = connection) { // the connection is unused - see comment in #collectRemoteNodes + // we have to close this connection before we notify listeners - this is mainly needed for test correctness + // since if we do it afterwards we might fail assertions that check if all high level connections are closed. + // from a code correctness perspective we could also close it afterwards. This try/with block will + // maintain the possibly exceptions thrown from within the try block and suppress the ones that are possible thrown + // by closing the connection + cancellableThreads.executeIO(() -> { + DiscoveryNodes nodes = response.getState().nodes(); + Iterable nodesIter = nodes.getNodes()::valuesIt; + for (DiscoveryNode node : nodesIter) { + if (nodePredicate.test(node) && connectedNodes.size() < maxNumRemoteConnections) { + try { + transportService.connectToNode(node, remoteProfile); // noop if node is connected + connectedNodes.add(node); + } catch (ConnectTransportException | IllegalStateException ex) { + // ISE if we fail the handshake with an version incompatible node + // fair enough we can't connect just move on + logger.debug((Supplier) + () -> new ParameterizedMessage("failed to connect to node {}", node), ex); + } + } + } + }); + } + listener.onResponse(null); + } catch (CancellableThreads.ExecutionCancelledException ex) { + listener.onFailure(ex); // we got canceled - fail the listener and step out + } catch (Exception ex) { + logger.warn((Supplier) + () -> new ParameterizedMessage("fetching nodes from external cluster {} failed", + clusterAlias), ex); + collectRemoteNodes(seedNodes, transportService, listener); + } + } + + @Override + public void handleException(TransportException exp) { + assert transportService.getThreadPool().getThreadContext().isSystemContext() == false : "context is a system context"; + logger.warn((Supplier) + () -> new ParameterizedMessage("fetching nodes from external cluster {} failed", clusterAlias), + exp); + try { + IOUtils.closeWhileHandlingException(connection); + } finally { + // once the connection is closed lets try the next node + collectRemoteNodes(seedNodes, transportService, listener); + } + } + + @Override + public String executor() { + return ThreadPool.Names.MANAGEMENT; + } + } + } + + boolean assertNoRunningConnections() { // for testing only + assert connectHandler.running.availablePermits() == 1; + return true; + } + + boolean isNodeConnected(final DiscoveryNode node) { + return connectedNodes.contains(node); + } + + DiscoveryNode getConnectedNode() { + return connectedNodes.get(); + } + + void addConnectedNode(DiscoveryNode node) { + connectedNodes.add(node); + } + + /** + * Fetches connection info for this connection + */ + public void getConnectionInfo(ActionListener listener) { + final Optional anyNode = connectedNodes.getAny(); + if (anyNode.isPresent() == false) { + // not connected we return immediately + RemoteConnectionInfo remoteConnectionStats = new RemoteConnectionInfo(clusterAlias, + Collections.emptyList(), Collections.emptyList(), maxNumRemoteConnections, 0, + RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings), skipUnavailable); + listener.onResponse(remoteConnectionStats); + } else { + NodesInfoRequest request = new NodesInfoRequest(); + request.clear(); + request.http(true); + + transportService.sendRequest(anyNode.get(), NodesInfoAction.NAME, request, new TransportResponseHandler() { + @Override + public NodesInfoResponse newInstance() { + return new NodesInfoResponse(); + } + + @Override + public void handleResponse(NodesInfoResponse response) { + Collection httpAddresses = new HashSet<>(); + for (NodeInfo info : response.getNodes()) { + if (connectedNodes.contains(info.getNode()) && info.getHttp() != null) { + httpAddresses.add(info.getHttp().getAddress().publishAddress()); + } + } + + if (httpAddresses.size() < maxNumRemoteConnections) { + // just in case non of the connected nodes have http enabled we get other http enabled nodes instead. + for (NodeInfo info : response.getNodes()) { + if (nodePredicate.test(info.getNode()) && info.getHttp() != null) { + httpAddresses.add(info.getHttp().getAddress().publishAddress()); + } + if (httpAddresses.size() == maxNumRemoteConnections) { + break; // once we have enough return... + } + } + } + RemoteConnectionInfo remoteConnectionInfo = new RemoteConnectionInfo(clusterAlias, + seedNodes.stream().map(DiscoveryNode::getAddress).collect(Collectors.toList()), new ArrayList<>(httpAddresses), + maxNumRemoteConnections, connectedNodes.size(), + RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings), skipUnavailable); + listener.onResponse(remoteConnectionInfo); + } + + @Override + public void handleException(TransportException exp) { + listener.onFailure(exp); + } + + @Override + public String executor() { + return ThreadPool.Names.SAME; + } + }); + } + + } + + int getNumNodesConnected() { + return connectedNodes.size(); + } + + private static class ConnectedNodes implements Supplier { + + private final Set nodeSet = new HashSet<>(); + private final String clusterAlias; + + private Iterator currentIterator = null; + + private ConnectedNodes(String clusterAlias) { + this.clusterAlias = clusterAlias; + } + + @Override + public synchronized DiscoveryNode get() { + ensureIteratorAvailable(); + if (currentIterator.hasNext()) { + return currentIterator.next(); + } else { + throw new IllegalStateException("No node available for cluster: " + clusterAlias); + } + } + + synchronized boolean remove(DiscoveryNode node) { + final boolean setRemoval = nodeSet.remove(node); + if (setRemoval) { + currentIterator = null; + } + return setRemoval; + } + + synchronized boolean add(DiscoveryNode node) { + final boolean added = nodeSet.add(node); + if (added) { + currentIterator = null; + } + return added; + } + + synchronized int size() { + return nodeSet.size(); + } + + synchronized boolean contains(DiscoveryNode node) { + return nodeSet.contains(node); + } + + synchronized Optional getAny() { + ensureIteratorAvailable(); + if (currentIterator.hasNext()) { + return Optional.of(currentIterator.next()); + } else { + return Optional.empty(); + } + } + + private synchronized void ensureIteratorAvailable() { + if (currentIterator == null) { + currentIterator = nodeSet.iterator(); + } else if (currentIterator.hasNext() == false && nodeSet.isEmpty() == false) { + // iterator rollover + currentIterator = nodeSet.iterator(); + } + } + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterService.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterService.java new file mode 100644 index 0000000..aaa0c41 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteClusterService.java @@ -0,0 +1,385 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.OriginalIndices; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsResponse; +import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.CountDown; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.transport.TransportException; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.settings.Setting.boolSetting; + +/** + * Basic service for accessing remote clusters via gateway nodes + */ +public final class RemoteClusterService extends RemoteClusterAware implements Closeable { + + /** + * The maximum number of connections that will be established to a remote cluster. For instance if there is only a single + * seed node, other nodes will be discovered up to the given number of nodes in this setting. The default is 3. + */ + public static final Setting REMOTE_CONNECTIONS_PER_CLUSTER = Setting.intSetting("search.remote.connections_per_cluster", + 3, 1, Setting.Property.NodeScope); + + /** + * The initial connect timeout for remote cluster connections + */ + public static final Setting REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING = + Setting.positiveTimeSetting("search.remote.initial_connect_timeout", TimeValue.timeValueSeconds(30), Setting.Property.NodeScope); + + /** + * The name of a node attribute to select nodes that should be connected to in the remote cluster. + * For instance a node can be configured with node.attr.gateway: true in order to be eligible as a gateway node between + * clusters. In that case search.remote.node.attr: gateway can be used to filter out other nodes in the remote cluster. + * The value of the setting is expected to be a boolean, true for nodes that can become gateways, false otherwise. + */ + public static final Setting REMOTE_NODE_ATTRIBUTE = Setting.simpleString("search.remote.node.attr", + Setting.Property.NodeScope); + + /** + * If true connecting to remote clusters is supported on this node. If false this node will not establish + * connections to any remote clusters configured. Search requests executed against this node (where this node is the coordinating node) + * will fail if remote cluster syntax is used as an index pattern. The default is true + */ + public static final Setting ENABLE_REMOTE_CLUSTERS = Setting.boolSetting("search.remote.connect", true, + Setting.Property.NodeScope); + + public static final Setting.AffixSetting REMOTE_CLUSTER_SKIP_UNAVAILABLE = + Setting.affixKeySetting("search.remote.", "skip_unavailable", + key -> boolSetting(key, false, Setting.Property.NodeScope, Setting.Property.Dynamic), REMOTE_CLUSTERS_SEEDS); + + private final TransportService transportService; + private final int numRemoteConnections; + private volatile Map remoteClusters = Collections.emptyMap(); + + RemoteClusterService(Settings settings, TransportService transportService) { + super(settings); + this.transportService = transportService; + numRemoteConnections = REMOTE_CONNECTIONS_PER_CLUSTER.get(settings); + } + + /** + * This method updates the list of remote clusters. It's intended to be used as an update consumer on the settings infrastructure + * @param seeds a cluster alias to discovery node mapping representing the remote clusters seeds nodes + * @param connectionListener a listener invoked once every configured cluster has been connected to + */ + private synchronized void updateRemoteClusters(Map> seeds, ActionListener connectionListener) { + if (seeds.containsKey(LOCAL_CLUSTER_GROUP_KEY)) { + throw new IllegalArgumentException("remote clusters must not have the empty string as its key"); + } + Map remoteClusters = new HashMap<>(); + if (seeds.isEmpty()) { + connectionListener.onResponse(null); + } else { + CountDown countDown = new CountDown(seeds.size()); + Predicate nodePredicate = (node) -> Version.CURRENT.isCompatible(node.getVersion()); + if (REMOTE_NODE_ATTRIBUTE.exists(settings)) { + // nodes can be tagged with node.attr.remote_gateway: true to allow a node to be a gateway node for + // cross cluster search + String attribute = REMOTE_NODE_ATTRIBUTE.get(settings); + nodePredicate = nodePredicate.and((node) -> Booleans.parseBoolean(node.getAttributes().getOrDefault(attribute, "false"))); + } + remoteClusters.putAll(this.remoteClusters); + for (Map.Entry> entry : seeds.entrySet()) { + RemoteClusterConnection remote = this.remoteClusters.get(entry.getKey()); + if (entry.getValue().isEmpty()) { // with no seed nodes we just remove the connection + try { + IOUtils.close(remote); + } catch (IOException e) { + logger.warn("failed to close remote cluster connections for cluster: " + entry.getKey(), e); + } + remoteClusters.remove(entry.getKey()); + continue; + } + + if (remote == null) { // this is a new cluster we have to add a new representation + remote = new RemoteClusterConnection(settings, entry.getKey(), entry.getValue(), transportService, numRemoteConnections, + nodePredicate); + remoteClusters.put(entry.getKey(), remote); + } + + // now update the seed nodes no matter if it's new or already existing + RemoteClusterConnection finalRemote = remote; + remote.updateSeedNodes(entry.getValue(), ActionListener.wrap( + response -> { + if (countDown.countDown()) { + connectionListener.onResponse(response); + } + }, + exception -> { + if (countDown.fastForward()) { + connectionListener.onFailure(exception); + } + if (finalRemote.isClosed() == false) { + logger.warn("failed to update seed list for cluster: " + entry.getKey(), exception); + } + })); + } + } + this.remoteClusters = Collections.unmodifiableMap(remoteClusters); + } + + /** + * Returns true if at least one remote cluster is configured + */ + public boolean isCrossClusterSearchEnabled() { + return remoteClusters.isEmpty() == false; + } + + boolean isRemoteNodeConnected(final String remoteCluster, final DiscoveryNode node) { + return remoteClusters.get(remoteCluster).isNodeConnected(node); + } + + public Map groupIndices(IndicesOptions indicesOptions, String[] indices, Predicate indexExists) { + Map originalIndicesMap = new HashMap<>(); + if (isCrossClusterSearchEnabled()) { + final Map> groupedIndices = groupClusterIndices(indices, indexExists); + for (Map.Entry> entry : groupedIndices.entrySet()) { + String clusterAlias = entry.getKey(); + List originalIndices = entry.getValue(); + originalIndicesMap.put(clusterAlias, + new OriginalIndices(originalIndices.toArray(new String[originalIndices.size()]), indicesOptions)); + } + if (originalIndicesMap.containsKey(LOCAL_CLUSTER_GROUP_KEY) == false) { + originalIndicesMap.put(LOCAL_CLUSTER_GROUP_KEY, new OriginalIndices(Strings.EMPTY_ARRAY, indicesOptions)); + } + } else { + originalIndicesMap.put(LOCAL_CLUSTER_GROUP_KEY, new OriginalIndices(indices, indicesOptions)); + } + return originalIndicesMap; + } + + /** + * Returns true iff the given cluster is configured as a remote cluster. Otherwise false + */ + boolean isRemoteClusterRegistered(String clusterName) { + return remoteClusters.containsKey(clusterName); + } + + public void collectSearchShards(IndicesOptions indicesOptions, String preference, String routing, + Map remoteIndicesByCluster, + ActionListener> listener) { + final CountDown responsesCountDown = new CountDown(remoteIndicesByCluster.size()); + final Map searchShardsResponses = new ConcurrentHashMap<>(); + final AtomicReference transportException = new AtomicReference<>(); + for (Map.Entry entry : remoteIndicesByCluster.entrySet()) { + final String clusterName = entry.getKey(); + RemoteClusterConnection remoteClusterConnection = remoteClusters.get(clusterName); + if (remoteClusterConnection == null) { + throw new IllegalArgumentException("no such remote cluster: " + clusterName); + } + final String[] indices = entry.getValue().indices(); + ClusterSearchShardsRequest searchShardsRequest = new ClusterSearchShardsRequest(indices) + .indicesOptions(indicesOptions).local(true).preference(preference) + .routing(routing); + remoteClusterConnection.fetchSearchShards(searchShardsRequest, + new ActionListener() { + @Override + public void onResponse(ClusterSearchShardsResponse clusterSearchShardsResponse) { + searchShardsResponses.put(clusterName, clusterSearchShardsResponse); + if (responsesCountDown.countDown()) { + TransportException exception = transportException.get(); + if (exception == null) { + listener.onResponse(searchShardsResponses); + } else { + listener.onFailure(transportException.get()); + } + } + } + + @Override + public void onFailure(Exception e) { + TransportException exception = new TransportException("unable to communicate with remote cluster [" + + clusterName + "]", e); + if (transportException.compareAndSet(null, exception) == false) { + exception = transportException.accumulateAndGet(exception, (previous, current) -> { + current.addSuppressed(previous); + return current; + }); + } + if (responsesCountDown.countDown()) { + listener.onFailure(exception); + } + } + }); + } + } + + /** + * Returns a connection to the given node on the given remote cluster + * @throws IllegalArgumentException if the remote cluster is unknown + */ + public Transport.Connection getConnection(DiscoveryNode node, String cluster) { + RemoteClusterConnection connection = remoteClusters.get(cluster); + if (connection == null) { + throw new IllegalArgumentException("no such remote cluster: " + cluster); + } + return connection.getConnection(node); + } + + /** + * Ensures that the given cluster alias is connected. If the cluster is connected this operation + * will invoke the listener immediately. + */ + public void ensureConnected(String clusterAlias, ActionListener listener) { + RemoteClusterConnection remoteClusterConnection = remoteClusters.get(clusterAlias); + if (remoteClusterConnection == null) { + throw new IllegalArgumentException("no such remote cluster: " + clusterAlias); + } + remoteClusterConnection.ensureConnected(listener); + } + + public Transport.Connection getConnection(String cluster) { + RemoteClusterConnection connection = remoteClusters.get(cluster); + if (connection == null) { + throw new IllegalArgumentException("no such remote cluster: " + cluster); + } + return connection.getConnection(); + } + + @Override + protected Set getRemoteClusterNames() { + return this.remoteClusters.keySet(); + } + + @Override + public void listenForUpdates(ClusterSettings clusterSettings) { + super.listenForUpdates(clusterSettings); + clusterSettings.addAffixUpdateConsumer(REMOTE_CLUSTER_SKIP_UNAVAILABLE, this::updateSkipUnavailable, + (clusterAlias, value) -> {}); + } + + synchronized void updateSkipUnavailable(String clusterAlias, Boolean skipUnavailable) { + RemoteClusterConnection remote = this.remoteClusters.get(clusterAlias); + if (remote != null) { + remote.updateSkipUnavailable(skipUnavailable); + } + } + + protected void updateRemoteCluster(String clusterAlias, List addresses) { + updateRemoteCluster(clusterAlias, addresses, ActionListener.wrap((x) -> {}, (x) -> {})); + } + + void updateRemoteCluster( + final String clusterAlias, + final List addresses, + final ActionListener connectionListener) { + final List nodes = addresses.stream().map(address -> { + final TransportAddress transportAddress = new TransportAddress(address); + final String id = clusterAlias + "#" + transportAddress.toString(); + final Version version = Version.CURRENT.minimumCompatibilityVersion(); + return new DiscoveryNode(id, transportAddress, version); + }).collect(Collectors.toList()); + updateRemoteClusters(Collections.singletonMap(clusterAlias, nodes), connectionListener); + } + + /** + * Connects to all remote clusters in a blocking fashion. This should be called on node startup to establish an initial connection + * to all configured seed nodes. + */ + void initializeRemoteClusters() { + final TimeValue timeValue = REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); + final PlainActionFuture future = new PlainActionFuture<>(); + Map> seeds = RemoteClusterAware.buildRemoteClustersSeeds(settings); + updateRemoteClusters(seeds, future); + try { + future.get(timeValue.millis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException ex) { + logger.warn("failed to connect to remote clusters within {}", timeValue.toString()); + } catch (Exception e) { + throw new IllegalStateException("failed to connect to remote clusters", e); + } + } + + @Override + public void close() throws IOException { + IOUtils.close(remoteClusters.values()); + } + + public void getRemoteConnectionInfos(ActionListener> listener) { + final Map remoteClusters = this.remoteClusters; + if (remoteClusters.isEmpty()) { + listener.onResponse(Collections.emptyList()); + } else { + final GroupedActionListener actionListener = new GroupedActionListener<>(listener, + remoteClusters.size(), Collections.emptyList()); + for (RemoteClusterConnection connection : remoteClusters.values()) { + connection.getConnectionInfo(actionListener); + } + } + } + + /** + * Collects all nodes of the given clusters and returns / passes a (clusterAlias, nodeId) to {@link DiscoveryNode} + * function on success. + */ + public void collectNodes(Set clusters, ActionListener> listener) { + Map remoteClusters = this.remoteClusters; + for (String cluster : clusters) { + if (remoteClusters.containsKey(cluster) == false) { + listener.onFailure(new IllegalArgumentException("no such remote cluster: [" + cluster + "]")); + return; + } + } + + final Map> clusterMap = new HashMap<>(); + CountDown countDown = new CountDown(clusters.size()); + Function nullFunction = s -> null; + for (final String cluster : clusters) { + RemoteClusterConnection connection = remoteClusters.get(cluster); + connection.collectNodes(new ActionListener>() { + @Override + public void onResponse(Function nodeLookup) { + synchronized (clusterMap) { + clusterMap.put(cluster, nodeLookup); + } + if (countDown.countDown()) { + listener.onResponse((clusterAlias, nodeId) + -> clusterMap.getOrDefault(clusterAlias, nullFunction).apply(nodeId)); + } + } + + @Override + public void onFailure(Exception e) { + if (countDown.fastForward()) { // we need to check if it's true since we could have multiple failures + listener.onFailure(e); + } + } + }); + } + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteConnectionInfo.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteConnectionInfo.java new file mode 100644 index 0000000..6809efc --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/RemoteConnectionInfo.java @@ -0,0 +1,112 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * This class encapsulates all remote cluster information to be rendered on + * _remote/info requests. + */ +public final class RemoteConnectionInfo implements ToXContentFragment, Writeable { + final List seedNodes; + final List httpAddresses; + final int connectionsPerCluster; + final TimeValue initialConnectionTimeout; + final int numNodesConnected; + final String clusterAlias; + final boolean skipUnavailable; + + RemoteConnectionInfo(String clusterAlias, List seedNodes, + List httpAddresses, + int connectionsPerCluster, int numNodesConnected, + TimeValue initialConnectionTimeout, boolean skipUnavailable) { + this.clusterAlias = clusterAlias; + this.seedNodes = seedNodes; + this.httpAddresses = httpAddresses; + this.connectionsPerCluster = connectionsPerCluster; + this.numNodesConnected = numNodesConnected; + this.initialConnectionTimeout = initialConnectionTimeout; + this.skipUnavailable = skipUnavailable; + } + + public RemoteConnectionInfo(StreamInput input) throws IOException { + seedNodes = input.readList(TransportAddress::new); + httpAddresses = input.readList(TransportAddress::new); + connectionsPerCluster = input.readVInt(); + initialConnectionTimeout = new TimeValue(input); + numNodesConnected = input.readVInt(); + clusterAlias = input.readString(); + if (input.getVersion().onOrAfter(Version.V_6_1_0)) { + skipUnavailable = input.readBoolean(); + } else { + skipUnavailable = false; + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(clusterAlias); + { + builder.startArray("seeds"); + for (TransportAddress addr : seedNodes) { + builder.value(addr.toString()); + } + builder.endArray(); + builder.startArray("http_addresses"); + for (TransportAddress addr : httpAddresses) { + builder.value(addr.toString()); + } + builder.endArray(); + builder.field("connected", numNodesConnected > 0); + builder.field("num_nodes_connected", numNodesConnected); + builder.field("max_connections_per_cluster", connectionsPerCluster); + builder.field("initial_connect_timeout", initialConnectionTimeout); + builder.field("skip_unavailable", skipUnavailable); + } + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeList(seedNodes); + out.writeList(httpAddresses); + out.writeVInt(connectionsPerCluster); + initialConnectionTimeout.writeTo(out); + out.writeVInt(numNodesConnected); + out.writeString(clusterAlias); + if (out.getVersion().onOrAfter(Version.V_6_1_0)) { + out.writeBoolean(skipUnavailable); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RemoteConnectionInfo that = (RemoteConnectionInfo) o; + return connectionsPerCluster == that.connectionsPerCluster && + numNodesConnected == that.numNodesConnected && + Objects.equals(seedNodes, that.seedNodes) && + Objects.equals(httpAddresses, that.httpAddresses) && + Objects.equals(initialConnectionTimeout, that.initialConnectionTimeout) && + Objects.equals(clusterAlias, that.clusterAlias) && + skipUnavailable == that.skipUnavailable; + } + + @Override + public int hashCode() { + return Objects.hash(seedNodes, httpAddresses, connectionsPerCluster, initialConnectionTimeout, + numNodesConnected, clusterAlias, skipUnavailable); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransport.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransport.java new file mode 100644 index 0000000..0a237e7 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransport.java @@ -0,0 +1,1808 @@ + +package org.xbib.elasticsearch.client.transport; + +import com.carrotsearch.hppc.IntHashSet; +import com.carrotsearch.hppc.IntSet; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.NotifyOnceListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.CheckedBiConsumer; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.bytes.CompositeBytesReference; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.compress.Compressor; +import org.elasticsearch.common.compress.CompressorFactory; +import org.elasticsearch.common.compress.NotCompressedException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.metrics.CounterMetric; +import org.elasticsearch.common.metrics.MeanMetric; +import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.network.NetworkUtils; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.PortsRange; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.concurrent.AbstractLifecycleRunnable; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; +import org.elasticsearch.common.util.concurrent.KeyedLock; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.monitor.jvm.JvmInfo; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.ActionNotFoundTransportException; +import org.elasticsearch.transport.BindTransportException; +import org.elasticsearch.transport.BytesTransportRequest; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.NodeNotConnectedException; +import org.elasticsearch.transport.RemoteTransportException; +import org.elasticsearch.transport.RequestHandlerRegistry; +import org.elasticsearch.transport.ResponseHandlerFailureTransportException; +import org.elasticsearch.transport.TcpChannel; +import org.elasticsearch.transport.TcpHeader; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportMessage; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportResponseOptions; +import org.elasticsearch.transport.TransportSerializationException; +import org.elasticsearch.transport.TransportStats; +import org.elasticsearch.transport.Transports; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StreamCorruptedException; +import java.io.UncheckedIOException; +import java.net.BindException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.channels.CancelledKeyException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.settings.Setting.affixKeySetting; +import static org.elasticsearch.common.settings.Setting.boolSetting; +import static org.elasticsearch.common.settings.Setting.intSetting; +import static org.elasticsearch.common.settings.Setting.listSetting; +import static org.elasticsearch.common.settings.Setting.timeSetting; +import static org.elasticsearch.common.transport.NetworkExceptionHelper.isCloseConnectionException; +import static org.elasticsearch.common.transport.NetworkExceptionHelper.isConnectException; +import static org.elasticsearch.common.util.concurrent.ConcurrentCollections.newConcurrentMap; + +public abstract class TcpTransport extends AbstractLifecycleComponent implements Transport { + + public static final String TRANSPORT_SERVER_WORKER_THREAD_NAME_PREFIX = "transport_server_worker"; + public static final String TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX = "transport_client_boss"; + + public static final Setting> HOST = + listSetting("transport.host", emptyList(), Function.identity(), Setting.Property.NodeScope); + public static final Setting> BIND_HOST = + listSetting("transport.bind_host", HOST, Function.identity(), Setting.Property.NodeScope); + public static final Setting> PUBLISH_HOST = + listSetting("transport.publish_host", HOST, Function.identity(), Setting.Property.NodeScope); + public static final Setting PORT = + new Setting<>("transport.tcp.port", "9300-9400", Function.identity(), Setting.Property.NodeScope); + public static final Setting PUBLISH_PORT = + intSetting("transport.publish_port", -1, -1, Setting.Property.NodeScope); + public static final String DEFAULT_PROFILE = "default"; + // the scheduled internal ping interval setting, defaults to disabled (-1) + public static final Setting PING_SCHEDULE = + timeSetting("transport.ping_schedule", TimeValue.timeValueSeconds(-1), Setting.Property.NodeScope); + public static final Setting CONNECTIONS_PER_NODE_RECOVERY = + intSetting("transport.connections_per_node.recovery", 2, 1, Setting.Property.NodeScope); + public static final Setting CONNECTIONS_PER_NODE_BULK = + intSetting("transport.connections_per_node.bulk", 3, 1, Setting.Property.NodeScope); + public static final Setting CONNECTIONS_PER_NODE_REG = + intSetting("transport.connections_per_node.reg", 6, 1, Setting.Property.NodeScope); + public static final Setting CONNECTIONS_PER_NODE_STATE = + intSetting("transport.connections_per_node.state", 1, 1, Setting.Property.NodeScope); + public static final Setting CONNECTIONS_PER_NODE_PING = + intSetting("transport.connections_per_node.ping", 1, 1, Setting.Property.NodeScope); + public static final Setting TCP_CONNECT_TIMEOUT = + timeSetting("transport.tcp.connect_timeout", NetworkService.TCP_CONNECT_TIMEOUT, Setting.Property.NodeScope); + public static final Setting TCP_NO_DELAY = + boolSetting("transport.tcp_no_delay", NetworkService.TCP_NO_DELAY, Setting.Property.NodeScope); + public static final Setting TCP_KEEP_ALIVE = + boolSetting("transport.tcp.keep_alive", NetworkService.TCP_KEEP_ALIVE, Setting.Property.NodeScope); + public static final Setting TCP_REUSE_ADDRESS = + boolSetting("transport.tcp.reuse_address", NetworkService.TCP_REUSE_ADDRESS, Setting.Property.NodeScope); + public static final Setting TCP_SEND_BUFFER_SIZE = + Setting.byteSizeSetting("transport.tcp.send_buffer_size", NetworkService.TCP_SEND_BUFFER_SIZE, + Setting.Property.NodeScope); + public static final Setting TCP_RECEIVE_BUFFER_SIZE = + Setting.byteSizeSetting("transport.tcp.receive_buffer_size", NetworkService.TCP_RECEIVE_BUFFER_SIZE, + Setting.Property.NodeScope); + + + public static final Setting.AffixSetting TCP_NO_DELAY_PROFILE = affixKeySetting("transport.profiles.", "tcp_no_delay", + key -> boolSetting(key, TcpTransport.TCP_NO_DELAY, Setting.Property.NodeScope)); + public static final Setting.AffixSetting TCP_KEEP_ALIVE_PROFILE = affixKeySetting("transport.profiles.", "tcp_keep_alive", + key -> boolSetting(key, TcpTransport.TCP_KEEP_ALIVE, Setting.Property.NodeScope)); + public static final Setting.AffixSetting TCP_REUSE_ADDRESS_PROFILE = affixKeySetting("transport.profiles.", "reuse_address", + key -> boolSetting(key, TcpTransport.TCP_REUSE_ADDRESS, Setting.Property.NodeScope)); + public static final Setting.AffixSetting TCP_SEND_BUFFER_SIZE_PROFILE = affixKeySetting("transport.profiles.", + "send_buffer_size", key -> Setting.byteSizeSetting(key, TcpTransport.TCP_SEND_BUFFER_SIZE, Setting.Property.NodeScope)); + public static final Setting.AffixSetting TCP_RECEIVE_BUFFER_SIZE_PROFILE = affixKeySetting("transport.profiles.", + "receive_buffer_size", key -> Setting.byteSizeSetting(key, TcpTransport.TCP_RECEIVE_BUFFER_SIZE, Setting.Property.NodeScope)); + + public static final Setting.AffixSetting> BIND_HOST_PROFILE = affixKeySetting("transport.profiles.", "bind_host", + key -> listSetting(key, BIND_HOST, Function.identity(), Setting.Property.NodeScope)); + public static final Setting.AffixSetting> PUBLISH_HOST_PROFILE = affixKeySetting("transport.profiles.", "publish_host", + key -> listSetting(key, PUBLISH_HOST, Function.identity(), Setting.Property.NodeScope)); + public static final Setting.AffixSetting PORT_PROFILE = affixKeySetting("transport.profiles.", "port", + key -> new Setting<>(key, PORT, Function.identity(), Setting.Property.NodeScope)); + public static final Setting.AffixSetting PUBLISH_PORT_PROFILE = affixKeySetting("transport.profiles.", "publish_port", + key -> intSetting(key, -1, -1, Setting.Property.NodeScope)); + + private static final long NINETY_PER_HEAP_SIZE = (long) (JvmInfo.jvmInfo().getMem().getHeapMax().getBytes() * 0.9); + public static final int PING_DATA_SIZE = -1; + private final CircuitBreakerService circuitBreakerService; + // package visibility for tests + protected final ScheduledPing scheduledPing; + private final TimeValue pingSchedule; + protected final ThreadPool threadPool; + private final BigArrays bigArrays; + protected final NetworkService networkService; + protected final Set profileSettings; + + private volatile TransportService transportService; + + private final ConcurrentMap profileBoundAddresses = newConcurrentMap(); + // node id to actual channel + private final ConcurrentMap connectedNodes = newConcurrentMap(); + private final Map> serverChannels = newConcurrentMap(); + private final Set acceptedChannels = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private final KeyedLock connectionLock = new KeyedLock<>(); + private final NamedWriteableRegistry namedWriteableRegistry; + + // this lock is here to make sure we close this transport and disconnect all the client nodes + // connections while no connect operations is going on... (this might help with 100% CPU when stopping the transport?) + private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); + protected final boolean compress; + private volatile BoundTransportAddress boundAddress; + private final String transportName; + protected final ConnectionProfile defaultConnectionProfile; + + private final ConcurrentMap pendingHandshakes = new ConcurrentHashMap<>(); + private final AtomicLong requestIdGenerator = new AtomicLong(); + private final CounterMetric numHandshakes = new CounterMetric(); + private static final String HANDSHAKE_ACTION_NAME = "internal:tcp/handshake"; + + private final MeanMetric readBytesMetric = new MeanMetric(); + private final MeanMetric transmittedBytesMetric = new MeanMetric(); + + public TcpTransport(String transportName, Settings settings, ThreadPool threadPool, BigArrays bigArrays, + CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, + NetworkService networkService) { + super(settings); + this.profileSettings = getProfileSettings(settings); + this.threadPool = threadPool; + this.bigArrays = bigArrays; + this.circuitBreakerService = circuitBreakerService; + this.scheduledPing = new ScheduledPing(); + this.pingSchedule = PING_SCHEDULE.get(settings); + this.namedWriteableRegistry = namedWriteableRegistry; + this.compress = Transport.TRANSPORT_TCP_COMPRESS.get(settings); + this.networkService = networkService; + this.transportName = transportName; + defaultConnectionProfile = buildDefaultConnectionProfile(settings); + } + + static ConnectionProfile buildDefaultConnectionProfile(Settings settings) { + int connectionsPerNodeRecovery = CONNECTIONS_PER_NODE_RECOVERY.get(settings); + int connectionsPerNodeBulk = CONNECTIONS_PER_NODE_BULK.get(settings); + int connectionsPerNodeReg = CONNECTIONS_PER_NODE_REG.get(settings); + int connectionsPerNodeState = CONNECTIONS_PER_NODE_STATE.get(settings); + int connectionsPerNodePing = CONNECTIONS_PER_NODE_PING.get(settings); + ConnectionProfile.Builder builder = new ConnectionProfile.Builder(); + builder.setConnectTimeout(TCP_CONNECT_TIMEOUT.get(settings)); + builder.setHandshakeTimeout(TCP_CONNECT_TIMEOUT.get(settings)); + builder.addConnections(connectionsPerNodeBulk, TransportRequestOptions.Type.BULK); + builder.addConnections(connectionsPerNodePing, TransportRequestOptions.Type.PING); + // if we are not master eligible we don't need a dedicated channel to publish the state + builder.addConnections(DiscoveryNode.isMasterNode(settings) ? connectionsPerNodeState : 0, TransportRequestOptions.Type.STATE); + // if we are not a data-node we don't need any dedicated channels for recovery + builder.addConnections(DiscoveryNode.isDataNode(settings) ? connectionsPerNodeRecovery : 0, TransportRequestOptions.Type.RECOVERY); + builder.addConnections(connectionsPerNodeReg, TransportRequestOptions.Type.REG); + return builder.build(); + } + + @Override + protected void doStart() { + if (pingSchedule.millis() > 0) { + threadPool.schedule(pingSchedule, ThreadPool.Names.GENERIC, scheduledPing); + } + } + + @Override + public CircuitBreaker getInFlightRequestBreaker() { + // We always obtain a fresh breaker to reflect changes to the breaker configuration. + return circuitBreakerService.getBreaker(CircuitBreaker.IN_FLIGHT_REQUESTS); + } + + @Override + public void setTransportService(TransportService service) { + if (service.getRequestHandler(HANDSHAKE_ACTION_NAME) != null) { + throw new IllegalStateException(HANDSHAKE_ACTION_NAME + " is a reserved request handler and must not be registered"); + } + this.transportService = service; + } + + private static class HandshakeResponseHandler implements TransportResponseHandler { + final AtomicReference versionRef = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference exceptionRef = new AtomicReference<>(); + final TcpChannel channel; + + HandshakeResponseHandler(TcpChannel channel) { + this.channel = channel; + } + + @Override + public VersionHandshakeResponse newInstance() { + return new VersionHandshakeResponse(); + } + + @Override + public void handleResponse(VersionHandshakeResponse response) { + final boolean success = versionRef.compareAndSet(null, response.version); + latch.countDown(); + assert success; + } + + @Override + public void handleException(TransportException exp) { + final boolean success = exceptionRef.compareAndSet(null, exp); + latch.countDown(); + assert success; + } + + @Override + public String executor() { + return ThreadPool.Names.SAME; + } + } + + public class ScheduledPing extends AbstractLifecycleRunnable { + + /** + * The magic number (must be lower than 0) for a ping message. This is handled + * specifically in {@link TcpTransport#validateMessageHeader}. + */ + private final BytesReference pingHeader; + final CounterMetric successfulPings = new CounterMetric(); + final CounterMetric failedPings = new CounterMetric(); + + public ScheduledPing() { + super(lifecycle, logger); + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeByte((byte) 'E'); + out.writeByte((byte) 'S'); + out.writeInt(PING_DATA_SIZE); + pingHeader = out.bytes(); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); // won't happen + } + } + + @Override + protected void doRunInLifecycle() throws Exception { + for (Map.Entry entry : connectedNodes.entrySet()) { + DiscoveryNode node = entry.getKey(); + NodeChannels channels = entry.getValue(); + for (TcpChannel channel : channels.getChannels()) { + internalSendMessage(channel, pingHeader, new SendMetricListener(pingHeader.length()) { + @Override + protected void innerInnerOnResponse(Void v) { + successfulPings.inc(); + } + + @Override + protected void innerOnFailure(Exception e) { + if (channel.isOpen()) { + logger.debug( + (Supplier) () -> new ParameterizedMessage("[{}] failed to send ping transport message", node), e); + failedPings.inc(); + } else { + logger.trace((Supplier) () -> + new ParameterizedMessage("[{}] failed to send ping transport message (channel closed)", node), e); + } + + } + }); + } + } + } + + public long getSuccessfulPings() { + return successfulPings.count(); + } + + public long getFailedPings() { + return failedPings.count(); + } + + @Override + protected void onAfterInLifecycle() { + try { + threadPool.schedule(pingSchedule, ThreadPool.Names.GENERIC, this); + } catch (EsRejectedExecutionException ex) { + if (ex.isExecutorShutdown()) { + logger.debug("couldn't schedule new ping execution, executor is shutting down", ex); + } else { + throw ex; + } + } + } + + @Override + public void onFailure(Exception e) { + if (lifecycle.stoppedOrClosed()) { + logger.trace("failed to send ping transport message", e); + } else { + logger.warn("failed to send ping transport message", e); + } + } + } + + public final class NodeChannels implements Connection { + private final Map typeMapping; + private final List channels; + private final DiscoveryNode node; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final Version version; + + NodeChannels(DiscoveryNode node, List channels, ConnectionProfile connectionProfile, Version handshakeVersion) { + this.node = node; + this.channels = Collections.unmodifiableList(channels); + assert channels.size() == connectionProfile.getNumConnections() : "expected channels size to be == " + + connectionProfile.getNumConnections() + " but was: [" + channels.size() + "]"; + typeMapping = new EnumMap<>(TransportRequestOptions.Type.class); + for (ConnectionProfile.ConnectionTypeHandle handle : connectionProfile.getHandles()) { + for (TransportRequestOptions.Type type : handle.getTypes()) + typeMapping.put(type, handle); + } + version = handshakeVersion; + } + + @Override + public Version getVersion() { + return version; + } + + public List getChannels() { + return channels; + } + + public TcpChannel channel(TransportRequestOptions.Type type) { + ConnectionProfile.ConnectionTypeHandle connectionTypeHandle = typeMapping.get(type); + if (connectionTypeHandle == null) { + throw new IllegalArgumentException("no type channel for [" + type + "]"); + } + return connectionTypeHandle.getChannel(channels); + } + + public boolean allChannelsOpen() { + return channels.stream().allMatch(TcpChannel::isOpen); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + try { + if (lifecycle.stopped()) { + /* We set SO_LINGER timeout to 0 to ensure that when we shutdown the node we don't + * have a gazillion connections sitting in TIME_WAIT to free up resources quickly. + * This is really the only part where we close the connection from the server side + * otherwise the client (node) initiates the TCP closing sequence which doesn't cause + * these issues. Setting this by default from the beginning can have unexpected + * side-effects an should be avoided, our protocol is designed in a way that clients + * close connection which is how it should be*/ + + channels.forEach(c -> { + try { + c.setSoLinger(0); + } catch (IOException e) { + logger.warn(new ParameterizedMessage("unexpected exception when setting SO_LINGER on channel {}", c), e); + } + }); + } + + boolean block = lifecycle.stopped() && Transports.isTransportThread(Thread.currentThread()) == false; + TcpChannel.closeChannels(channels, block); + } finally { + transportService.onConnectionClosed(this); + } + } + } + + @Override + public DiscoveryNode getNode() { + return this.node; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + if (closed.get()) { + throw new NodeNotConnectedException(node, "connection already closed"); + } + TcpChannel channel = channel(options.type()); + sendRequestToChannel(this.node, channel, requestId, action, request, options, getVersion(), (byte) 0); + } + + boolean isClosed() { + return closed.get(); + } + } + + @Override + public boolean nodeConnected(DiscoveryNode node) { + return connectedNodes.containsKey(node); + } + + @Override + public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile, + CheckedBiConsumer connectionValidator) + throws ConnectTransportException { + connectionProfile = resolveConnectionProfile(connectionProfile); + if (node == null) { + throw new ConnectTransportException(null, "can't connect to a null node"); + } + closeLock.readLock().lock(); // ensure we don't open connections while we are closing + try { + ensureOpen(); + try (Releasable ignored = connectionLock.acquire(node.getId())) { + NodeChannels nodeChannels = connectedNodes.get(node); + if (nodeChannels != null) { + return; + } + boolean success = false; + try { + nodeChannels = openConnection(node, connectionProfile); + connectionValidator.accept(nodeChannels, connectionProfile); + // we acquire a connection lock, so no way there is an existing connection + connectedNodes.put(node, nodeChannels); + if (logger.isDebugEnabled()) { + logger.debug("connected to node [{}]", node); + } + try { + transportService.onNodeConnected(node); + } finally { + if (nodeChannels.isClosed()) { + // we got closed concurrently due to a disconnect or some other event on the channel. + // the close callback will close the NodeChannel instance first and then try to remove + // the connection from the connected nodes. It will NOT acquire the connectionLock for + // the node to prevent any blocking calls on network threads. Yet, we still establish a happens + // before relationship to the connectedNodes.put since we check if we can remove the + // (DiscoveryNode, NodeChannels) tuple from the map after we closed. Here we check if it's closed an if so we + // try to remove it first either way one of the two wins even if the callback has run before we even added the + // tuple to the map since in that case we remove it here again + if (connectedNodes.remove(node, nodeChannels)) { + transportService.onNodeDisconnected(node); + } + throw new NodeNotConnectedException(node, "connection concurrently closed"); + } + } + success = true; + } catch (ConnectTransportException e) { + throw e; + } catch (Exception e) { + throw new ConnectTransportException(node, "general node connection failure", e); + } finally { + if (success == false) { // close the connection if there is a failure + logger.trace( + (Supplier) () -> new ParameterizedMessage( + "failed to connect to [{}], cleaning dangling connections", node)); + IOUtils.closeWhileHandlingException(nodeChannels); + } + } + } + } finally { + closeLock.readLock().unlock(); + } + } + + /** + * takes a {@link ConnectionProfile} that have been passed as a parameter to the public methods + * and resolves it to a fully specified (i.e., no nulls) profile + */ + static ConnectionProfile resolveConnectionProfile(@Nullable ConnectionProfile connectionProfile, + ConnectionProfile defaultConnectionProfile) { + Objects.requireNonNull(defaultConnectionProfile); + if (connectionProfile == null) { + return defaultConnectionProfile; + } else if (connectionProfile.getConnectTimeout() != null && connectionProfile.getHandshakeTimeout() != null) { + return connectionProfile; + } else { + ConnectionProfile.Builder builder = new ConnectionProfile.Builder(connectionProfile); + if (connectionProfile.getConnectTimeout() == null) { + builder.setConnectTimeout(defaultConnectionProfile.getConnectTimeout()); + } + if (connectionProfile.getHandshakeTimeout() == null) { + builder.setHandshakeTimeout(defaultConnectionProfile.getHandshakeTimeout()); + } + return builder.build(); + } + } + + protected ConnectionProfile resolveConnectionProfile(ConnectionProfile connectionProfile) { + return resolveConnectionProfile(connectionProfile, defaultConnectionProfile); + } + + @Override + public final NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { + if (node == null) { + throw new ConnectTransportException(null, "can't open connection to a null node"); + } + boolean success = false; + NodeChannels nodeChannels = null; + connectionProfile = resolveConnectionProfile(connectionProfile); + closeLock.readLock().lock(); // ensure we don't open connections while we are closing + try { + ensureOpen(); + try { + int numConnections = connectionProfile.getNumConnections(); + assert numConnections > 0 : "A connection profile must be configured with at least one connection"; + List channels = new ArrayList<>(numConnections); + List> connectionFutures = new ArrayList<>(numConnections); + for (int i = 0; i < numConnections; ++i) { + try { + PlainActionFuture connectFuture = PlainActionFuture.newFuture(); + connectionFutures.add(connectFuture); + TcpChannel channel = initiateChannel(node, connectionProfile.getConnectTimeout(), connectFuture); + logger.trace(() -> new ParameterizedMessage("Tcp transport client channel opened: {}", channel)); + channels.add(channel); + } catch (Exception e) { + // If there was an exception when attempting to instantiate the raw channels, we close all of the channels + TcpChannel.closeChannels(channels, false); + throw e; + } + } + + // If we make it past the block above, we successfully instantiated all of the channels + try { + TcpChannel.awaitConnected(node, connectionFutures, connectionProfile.getConnectTimeout()); + } catch (Exception ex) { + TcpChannel.closeChannels(channels, false); + throw ex; + } + + // If we make it past the block above, we have successfully established connections for all of the channels + final TcpChannel handshakeChannel = channels.get(0); // one channel is guaranteed by the connection profile + handshakeChannel.addCloseListener(ActionListener.wrap(() -> cancelHandshakeForChannel(handshakeChannel))); + Version version; + try { + version = executeHandshake(node, handshakeChannel, connectionProfile.getHandshakeTimeout()); + } catch (Exception ex) { + TcpChannel.closeChannels(channels, false); + throw ex; + } + + // If we make it past the block above, we have successfully completed the handshake and the connection is now open. + // At this point we should construct the connection, notify the transport service, and attach close listeners to the + // underlying channels. + nodeChannels = new NodeChannels(node, channels, connectionProfile, version); + transportService.onConnectionOpened(nodeChannels); + final NodeChannels finalNodeChannels = nodeChannels; + final AtomicBoolean runOnce = new AtomicBoolean(false); + Consumer onClose = c -> { + assert c.isOpen() == false : "channel is still open when onClose is called"; + // we only need to disconnect from the nodes once since all other channels + // will also try to run this we protect it from running multiple times. + if (runOnce.compareAndSet(false, true)) { + disconnectFromNodeCloseAndNotify(node, finalNodeChannels); + } + }; + + nodeChannels.channels.forEach(ch -> ch.addCloseListener(ActionListener.wrap(() -> onClose.accept(ch)))); + + if (nodeChannels.allChannelsOpen() == false) { + throw new ConnectTransportException(node, "a channel closed while connecting"); + } + success = true; + return nodeChannels; + } catch (ConnectTransportException e) { + throw e; + } catch (Exception e) { + // ConnectTransportExceptions are handled specifically on the caller end - we wrap the actual exception to ensure + // only relevant exceptions are logged on the caller end.. this is the same as in connectToNode + throw new ConnectTransportException(node, "general node connection failure", e); + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException(nodeChannels); + } + } + } finally { + closeLock.readLock().unlock(); + } + } + + private void disconnectFromNodeCloseAndNotify(DiscoveryNode node, NodeChannels nodeChannels) { + assert nodeChannels != null : "nodeChannels must not be null"; + try { + IOUtils.closeWhileHandlingException(nodeChannels); + } finally { + if (closeLock.readLock().tryLock()) { + try { + if (connectedNodes.remove(node, nodeChannels)) { + transportService.onNodeDisconnected(node); + } + } finally { + closeLock.readLock().unlock(); + } + } + } + } + + @Override + public NodeChannels getConnection(DiscoveryNode node) { + NodeChannels nodeChannels = connectedNodes.get(node); + if (nodeChannels == null) { + throw new NodeNotConnectedException(node, "Node not connected"); + } + return nodeChannels; + } + + @Override + public void disconnectFromNode(DiscoveryNode node) { + closeLock.readLock().lock(); + NodeChannels nodeChannels = null; + try (Releasable ignored = connectionLock.acquire(node.getId())) { + nodeChannels = connectedNodes.remove(node); + } finally { + closeLock.readLock().unlock(); + if (nodeChannels != null) { // if we found it and removed it we close and notify + IOUtils.closeWhileHandlingException(nodeChannels, () -> transportService.onNodeDisconnected(node)); + } + } + } + + protected Version getCurrentVersion() { + // this is just for tests to mock stuff like the nodes version - tests can override this internally + return Version.CURRENT; + } + + @Override + public BoundTransportAddress boundAddress() { + return this.boundAddress; + } + + @Override + public Map profileBoundAddresses() { + return unmodifiableMap(new HashMap<>(profileBoundAddresses)); + } + + @Override + public List getLocalAddresses() { + List local = new ArrayList<>(); + local.add("127.0.0.1"); + // check if v6 is supported, if so, v4 will also work via mapped addresses. + if (NetworkUtils.SUPPORTS_V6) { + local.add("[::1]"); // may get ports appended! + } + return local; + } + + protected void bindServer(ProfileSettings profileSettings) { + // Bind and start to accept incoming connections. + InetAddress hostAddresses[]; + List profileBindHosts = profileSettings.bindHosts; + try { + hostAddresses = networkService.resolveBindHostAddresses(profileBindHosts.toArray(Strings.EMPTY_ARRAY)); + } catch (IOException e) { + throw new BindTransportException("Failed to resolve host " + profileBindHosts, e); + } + if (logger.isDebugEnabled()) { + String[] addresses = new String[hostAddresses.length]; + for (int i = 0; i < hostAddresses.length; i++) { + addresses[i] = NetworkAddress.format(hostAddresses[i]); + } + logger.debug("binding server bootstrap to: {}", (Object) addresses); + } + + assert hostAddresses.length > 0; + + List boundAddresses = new ArrayList<>(); + for (InetAddress hostAddress : hostAddresses) { + boundAddresses.add(bindToPort(profileSettings.profileName, hostAddress, profileSettings.portOrRange)); + } + + final BoundTransportAddress boundTransportAddress = createBoundTransportAddress(profileSettings, boundAddresses); + + if (profileSettings.isDefaultProfile) { + this.boundAddress = boundTransportAddress; + } else { + profileBoundAddresses.put(profileSettings.profileName, boundTransportAddress); + } + } + + protected InetSocketAddress bindToPort(final String name, final InetAddress hostAddress, String port) { + PortsRange portsRange = new PortsRange(port); + final AtomicReference lastException = new AtomicReference<>(); + final AtomicReference boundSocket = new AtomicReference<>(); + boolean success = portsRange.iterate(portNumber -> { + try { + TcpChannel channel = bind(name, new InetSocketAddress(hostAddress, portNumber)); + synchronized (serverChannels) { + List list = serverChannels.get(name); + if (list == null) { + list = new ArrayList<>(); + serverChannels.put(name, list); + } + list.add(channel); + boundSocket.set(channel.getLocalAddress()); + } + } catch (Exception e) { + lastException.set(e); + return false; + } + return true; + }); + if (!success) { + throw new BindTransportException("Failed to bind to [" + port + "]", lastException.get()); + } + + if (logger.isDebugEnabled()) { + logger.debug("Bound profile [{}] to address {{}}", name, NetworkAddress.format(boundSocket.get())); + } + + return boundSocket.get(); + } + + private BoundTransportAddress createBoundTransportAddress(ProfileSettings profileSettings, + List boundAddresses) { + String[] boundAddressesHostStrings = new String[boundAddresses.size()]; + TransportAddress[] transportBoundAddresses = new TransportAddress[boundAddresses.size()]; + for (int i = 0; i < boundAddresses.size(); i++) { + InetSocketAddress boundAddress = boundAddresses.get(i); + boundAddressesHostStrings[i] = boundAddress.getHostString(); + transportBoundAddresses[i] = new TransportAddress(boundAddress); + } + + List publishHosts = profileSettings.publishHosts; + if (profileSettings.isDefaultProfile == false && publishHosts.isEmpty()) { + publishHosts = Arrays.asList(boundAddressesHostStrings); + } + if (publishHosts.isEmpty()) { + publishHosts = NetworkService.GLOBAL_NETWORK_PUBLISHHOST_SETTING.get(settings); + } + + final InetAddress publishInetAddress; + try { + publishInetAddress = networkService.resolvePublishHostAddresses(publishHosts.toArray(Strings.EMPTY_ARRAY)); + } catch (Exception e) { + throw new BindTransportException("Failed to resolve publish address", e); + } + + final int publishPort = resolvePublishPort(profileSettings, boundAddresses, publishInetAddress); + final TransportAddress publishAddress = new TransportAddress(new InetSocketAddress(publishInetAddress, publishPort)); + return new BoundTransportAddress(transportBoundAddresses, publishAddress); + } + + // package private for tests + public static int resolvePublishPort(ProfileSettings profileSettings, List boundAddresses, + InetAddress publishInetAddress) { + int publishPort = profileSettings.publishPort; + + // if port not explicitly provided, search for port of address in boundAddresses that matches publishInetAddress + if (publishPort < 0) { + for (InetSocketAddress boundAddress : boundAddresses) { + InetAddress boundInetAddress = boundAddress.getAddress(); + if (boundInetAddress.isAnyLocalAddress() || boundInetAddress.equals(publishInetAddress)) { + publishPort = boundAddress.getPort(); + break; + } + } + } + + // if no matching boundAddress found, check if there is a unique port for all bound addresses + if (publishPort < 0) { + final IntSet ports = new IntHashSet(); + for (InetSocketAddress boundAddress : boundAddresses) { + ports.add(boundAddress.getPort()); + } + if (ports.size() == 1) { + publishPort = ports.iterator().next().value; + } + } + + if (publishPort < 0) { + String profileExplanation = profileSettings.isDefaultProfile ? "" : " for profile " + profileSettings.profileName; + throw new BindTransportException("Failed to auto-resolve publish port" + profileExplanation + ", multiple bound addresses " + + boundAddresses + " with distinct ports and none of them matched the publish address (" + publishInetAddress + "). " + + "Please specify a unique port by setting " + PORT.getKey() + " or " + + PUBLISH_PORT.getKey()); + } + return publishPort; + } + + @Override + public TransportAddress[] addressesFromString(String address, int perAddressLimit) throws UnknownHostException { + return parse(address, settings.get("transport.profiles.default.port", PORT.get(settings)), perAddressLimit); + } + + // this code is a take on guava's HostAndPort, like a HostAndPortRange + + // pattern for validating ipv6 bracket addresses. + // not perfect, but PortsRange should take care of any port range validation, not a regex + private static final Pattern BRACKET_PATTERN = Pattern.compile("^\\[(.*:.*)\\](?::([\\d\\-]*))?$"); + + /** parse a hostname+port range spec into its equivalent addresses */ + static TransportAddress[] parse(String hostPortString, String defaultPortRange, int perAddressLimit) throws UnknownHostException { + Objects.requireNonNull(hostPortString); + String host; + String portString = null; + + if (hostPortString.startsWith("[")) { + // Parse a bracketed host, typically an IPv6 literal. + Matcher matcher = BRACKET_PATTERN.matcher(hostPortString); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid bracketed host/port range: " + hostPortString); + } + host = matcher.group(1); + portString = matcher.group(2); // could be null + } else { + int colonPos = hostPortString.indexOf(':'); + if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) { + // Exactly 1 colon. Split into host:port. + host = hostPortString.substring(0, colonPos); + portString = hostPortString.substring(colonPos + 1); + } else { + // 0 or 2+ colons. Bare hostname or IPv6 literal. + host = hostPortString; + // 2+ colons and not bracketed: exception + if (colonPos >= 0) { + throw new IllegalArgumentException("IPv6 addresses must be bracketed: " + hostPortString); + } + } + } + + // if port isn't specified, fill with the default + if (portString == null || portString.isEmpty()) { + portString = defaultPortRange; + } + + // generate address for each port in the range + Set addresses = new HashSet<>(Arrays.asList(InetAddress.getAllByName(host))); + List transportAddresses = new ArrayList<>(); + int[] ports = new PortsRange(portString).ports(); + int limit = Math.min(ports.length, perAddressLimit); + for (int i = 0; i < limit; i++) { + for (InetAddress address : addresses) { + transportAddresses.add(new TransportAddress(address, ports[i])); + } + } + return transportAddresses.toArray(new TransportAddress[transportAddresses.size()]); + } + + @Override + protected final void doClose() { + } + + @Override + protected final void doStop() { + final CountDownLatch latch = new CountDownLatch(1); + // make sure we run it on another thread than a possible IO handler thread + threadPool.generic().execute(() -> { + closeLock.writeLock().lock(); + try { + // first stop to accept any incoming connections so nobody can connect to this transport + for (Map.Entry> entry : serverChannels.entrySet()) { + String profile = entry.getKey(); + List channels = entry.getValue(); + ActionListener closeFailLogger = ActionListener.wrap(c -> {}, + e -> logger.warn(() -> new ParameterizedMessage("Error closing serverChannel for profile [{}]", profile), e)); + channels.forEach(c -> c.addCloseListener(closeFailLogger)); + TcpChannel.closeChannels(channels, true); + } + serverChannels.clear(); + + // close all of the incoming channels. The closeChannels method takes a list so we must convert the set. + TcpChannel.closeChannels(new ArrayList<>(acceptedChannels), true); + acceptedChannels.clear(); + + + // we are holding a write lock so nobody modifies the connectedNodes / openConnections map - it's safe to first close + // all instances and then clear them maps + Iterator> iterator = connectedNodes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + try { + IOUtils.closeWhileHandlingException(next.getValue()); + transportService.onNodeDisconnected(next.getKey()); + } finally { + iterator.remove(); + } + } + stopInternal(); + } finally { + closeLock.writeLock().unlock(); + latch.countDown(); + } + }); + + try { + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // ignore + } + } + + protected void onException(TcpChannel channel, Exception e) { + if (!lifecycle.started()) { + // just close and ignore - we are already stopped and just need to make sure we release all resources + TcpChannel.closeChannel(channel, false); + return; + } + + if (isCloseConnectionException(e)) { + logger.trace( + (Supplier) () -> new ParameterizedMessage( + "close connection exception caught on transport layer [{}], disconnecting from relevant node", + channel), + e); + // close the channel, which will cause a node to be disconnected if relevant + TcpChannel.closeChannel(channel, false); + } else if (isConnectException(e)) { + logger.trace((Supplier) () -> new ParameterizedMessage("connect exception caught on transport layer [{}]", channel), e); + // close the channel as safe measure, which will cause a node to be disconnected if relevant + TcpChannel.closeChannel(channel, false); + } else if (e instanceof BindException) { + logger.trace((Supplier) () -> new ParameterizedMessage("bind exception caught on transport layer [{}]", channel), e); + // close the channel as safe measure, which will cause a node to be disconnected if relevant + TcpChannel.closeChannel(channel, false); + } else if (e instanceof CancelledKeyException) { + logger.trace( + (Supplier) () -> new ParameterizedMessage( + "cancelled key exception caught on transport layer [{}], disconnecting from relevant node", + channel), + e); + // close the channel as safe measure, which will cause a node to be disconnected if relevant + TcpChannel.closeChannel(channel, false); + } else if (e instanceof TcpTransport.HttpOnTransportException) { + // in case we are able to return data, serialize the exception content and sent it back to the client + if (channel.isOpen()) { + BytesArray message = new BytesArray(e.getMessage().getBytes(StandardCharsets.UTF_8)); + final SendMetricListener closeChannel = new SendMetricListener(message.length()) { + @Override + protected void innerInnerOnResponse(Void v) { + TcpChannel.closeChannel(channel, false); + } + + @Override + protected void innerOnFailure(Exception e) { + logger.debug("failed to send message to httpOnTransport channel", e); + TcpChannel.closeChannel(channel, false); + } + }; + internalSendMessage(channel, message, closeChannel); + } + } else { + logger.warn( + (Supplier) () -> new ParameterizedMessage("exception caught on transport layer [{}], closing connection", channel), e); + // close the channel, which will cause a node to be disconnected if relevant + TcpChannel.closeChannel(channel, false); + } + } + + protected void serverAcceptedChannel(TcpChannel channel) { + boolean addedOnThisCall = acceptedChannels.add(channel); + assert addedOnThisCall : "Channel should only be added to accept channel set once"; + channel.addCloseListener(ActionListener.wrap(() -> acceptedChannels.remove(channel))); + logger.trace(() -> new ParameterizedMessage("Tcp transport channel accepted: {}", channel)); + } + + /** + * Binds to the given {@link InetSocketAddress} + * + * @param name the profile name + * @param address the address to bind to + */ + protected abstract TcpChannel bind(String name, InetSocketAddress address) throws IOException; + + /** + * Initiate a single tcp socket channel to a node. Implementations do not have to observe the connectTimeout. + * It is provided for synchronous connection implementations. + * + * @param node the node + * @param connectTimeout the connection timeout + * @param connectListener listener to be called when connection complete + * @return the pending connection + * @throws IOException if an I/O exception occurs while opening the channel + */ + protected abstract TcpChannel initiateChannel(DiscoveryNode node, TimeValue connectTimeout, ActionListener connectListener) + throws IOException; + + /** + * Called to tear down internal resources + */ + protected void stopInternal() { + } + + public boolean canCompress(TransportRequest request) { + return compress && (!(request instanceof BytesTransportRequest)); + } + + private void sendRequestToChannel(final DiscoveryNode node, final TcpChannel channel, final long requestId, final String action, + final TransportRequest request, TransportRequestOptions options, Version channelVersion, + byte status) throws IOException, + TransportException { + if (compress) { + options = TransportRequestOptions.builder(options).withCompress(true).build(); + } + + // only compress if asked and the request is not bytes. Otherwise only + // the header part is compressed, and the "body" can't be extracted as compressed + final boolean compressMessage = options.compress() && canCompress(request); + + status = TransportStatus.setRequest(status); + ReleasableBytesStreamOutput bStream = new ReleasableBytesStreamOutput(bigArrays); + final CompressibleBytesOutputStream stream = new CompressibleBytesOutputStream(bStream, compressMessage); + boolean addedReleaseListener = false; + try { + if (compressMessage) { + status = TransportStatus.setCompress(status); + } + + // we pick the smallest of the 2, to support both backward and forward compatibility + // note, this is the only place we need to do this, since from here on, we use the serialized version + // as the version to use also when the node receiving this request will send the response with + Version version = Version.min(getCurrentVersion(), channelVersion); + + stream.setVersion(version); + threadPool.getThreadContext().writeTo(stream); + stream.writeString(action); + BytesReference message = buildMessage(requestId, status, node.getVersion(), request, stream); + final TransportRequestOptions finalOptions = options; + // this might be called in a different thread + SendListener onRequestSent = new SendListener(channel, stream, + () -> transportService.onRequestSent(node, requestId, action, request, finalOptions), message.length()); + internalSendMessage(channel, message, onRequestSent); + addedReleaseListener = true; + } finally { + if (!addedReleaseListener) { + IOUtils.close(stream); + } + } + } + + /** + * sends a message to the given channel, using the given callbacks. + */ + private void internalSendMessage(TcpChannel channel, BytesReference message, SendMetricListener listener) { + try { + channel.sendMessage(message, listener); + } catch (Exception ex) { + // call listener to ensure that any resources are released + listener.onFailure(ex); + onException(channel, ex); + } + } + + /** + * Sends back an error response to the caller via the given channel + * + * @param nodeVersion the caller node version + * @param channel the channel to send the response to + * @param error the error to return + * @param requestId the request ID this response replies to + * @param action the action this response replies to + */ + public void sendErrorResponse(Version nodeVersion, TcpChannel channel, final Exception error, final long requestId, + final String action) throws IOException { + try (BytesStreamOutput stream = new BytesStreamOutput()) { + stream.setVersion(nodeVersion); + RemoteTransportException tx = new RemoteTransportException( + nodeName(), new TransportAddress(channel.getLocalAddress()), action, error); + threadPool.getThreadContext().writeTo(stream); + stream.writeException(tx); + byte status = 0; + status = TransportStatus.setResponse(status); + status = TransportStatus.setError(status); + final BytesReference bytes = stream.bytes(); + final BytesReference header = buildHeader(requestId, status, nodeVersion, bytes.length()); + CompositeBytesReference message = new CompositeBytesReference(header, bytes); + SendListener onResponseSent = new SendListener(channel, null, + () -> transportService.onResponseSent(requestId, action, error), message.length()); + internalSendMessage(channel, message, onResponseSent); + } + } + + /** + * Sends the response to the given channel. This method should be used to send {@link TransportResponse} objects back to the caller. + * + * @see #sendErrorResponse(Version, TcpChannel, Exception, long, String) for sending back errors to the caller + */ + public void sendResponse(Version nodeVersion, TcpChannel channel, final TransportResponse response, final long requestId, + final String action, TransportResponseOptions options) throws IOException { + sendResponse(nodeVersion, channel, response, requestId, action, options, (byte) 0); + } + + private void sendResponse(Version nodeVersion, TcpChannel channel, final TransportResponse response, final long requestId, + final String action, TransportResponseOptions options, byte status) throws IOException { + if (compress) { + options = TransportResponseOptions.builder(options).withCompress(true).build(); + } + status = TransportStatus.setResponse(status); // TODO share some code with sendRequest + ReleasableBytesStreamOutput bStream = new ReleasableBytesStreamOutput(bigArrays); + CompressibleBytesOutputStream stream = new CompressibleBytesOutputStream(bStream, options.compress()); + boolean addedReleaseListener = false; + try { + if (options.compress()) { + status = TransportStatus.setCompress(status); + } + threadPool.getThreadContext().writeTo(stream); + stream.setVersion(nodeVersion); + BytesReference message = buildMessage(requestId, status, nodeVersion, response, stream); + + final TransportResponseOptions finalOptions = options; + // this might be called in a different thread + SendListener listener = new SendListener(channel, stream, + () -> transportService.onResponseSent(requestId, action, response, finalOptions), message.length()); + internalSendMessage(channel, message, listener); + addedReleaseListener = true; + } finally { + if (!addedReleaseListener) { + IOUtils.close(stream); + } + } + } + + /** + * Writes the Tcp message header into a bytes reference. + * + * @param requestId the request ID + * @param status the request status + * @param protocolVersion the protocol version used to serialize the data in the message + * @param length the payload length in bytes + * @see TcpHeader + */ + final BytesReference buildHeader(long requestId, byte status, Version protocolVersion, int length) throws IOException { + try (BytesStreamOutput headerOutput = new BytesStreamOutput(TcpHeader.HEADER_SIZE)) { + headerOutput.setVersion(protocolVersion); + TcpHeader.writeHeader(headerOutput, requestId, status, protocolVersion, length); + final BytesReference bytes = headerOutput.bytes(); + assert bytes.length() == TcpHeader.HEADER_SIZE : "header size mismatch expected: " + TcpHeader.HEADER_SIZE + " but was: " + + bytes.length(); + return bytes; + } + } + + /** + * Serializes the given message into a bytes representation + */ + private BytesReference buildMessage(long requestId, byte status, Version nodeVersion, TransportMessage message, + CompressibleBytesOutputStream stream) throws IOException { + final BytesReference zeroCopyBuffer; + if (message instanceof BytesTransportRequest) { // what a shitty optimization - we should use a direct send method instead + BytesTransportRequest bRequest = (BytesTransportRequest) message; + assert nodeVersion.equals(bRequest.version()); + bRequest.writeThin(stream); + zeroCopyBuffer = bRequest.bytes(); + } else { + message.writeTo(stream); + zeroCopyBuffer = BytesArray.EMPTY; + } + // we have to call materializeBytes() here before accessing the bytes. A CompressibleBytesOutputStream + // might be implementing compression. And materializeBytes() ensures that some marker bytes (EOS marker) + // are written. Otherwise we barf on the decompressing end when we read past EOF on purpose in the + // #validateRequest method. this might be a problem in deflate after all but it's important to write + // the marker bytes. + final BytesReference messageBody = stream.materializeBytes(); + final BytesReference header = buildHeader(requestId, status, stream.getVersion(), messageBody.length() + zeroCopyBuffer.length()); + return new CompositeBytesReference(header, messageBody, zeroCopyBuffer); + } + + /** + * Validates the first N bytes of the message header and returns false if the message is + * a ping message and has no payload ie. isn't a real user level message. + * + * @throws IllegalStateException if the message is too short, less than the header or less that the header plus the message size + * @throws HttpOnTransportException if the message has no valid header and appears to be a HTTP message + * @throws IllegalArgumentException if the message is greater that the maximum allowed frame size. This is dependent on the available + * memory. + */ + public static boolean validateMessageHeader(BytesReference buffer) throws IOException { + final int sizeHeaderLength = TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE; + if (buffer.length() < sizeHeaderLength) { + throw new IllegalStateException("message size must be >= to the header size"); + } + int offset = 0; + if (buffer.get(offset) != 'E' || buffer.get(offset + 1) != 'S') { + // special handling for what is probably HTTP + if (bufferStartsWith(buffer, offset, "GET ") || + bufferStartsWith(buffer, offset, "POST ") || + bufferStartsWith(buffer, offset, "PUT ") || + bufferStartsWith(buffer, offset, "HEAD ") || + bufferStartsWith(buffer, offset, "DELETE ") || + bufferStartsWith(buffer, offset, "OPTIONS ") || + bufferStartsWith(buffer, offset, "PATCH ") || + bufferStartsWith(buffer, offset, "TRACE ")) { + + throw new HttpOnTransportException("This is not a HTTP port"); + } + + // we have 6 readable bytes, show 4 (should be enough) + throw new StreamCorruptedException("invalid internal transport message format, got (" + + Integer.toHexString(buffer.get(offset) & 0xFF) + "," + + Integer.toHexString(buffer.get(offset + 1) & 0xFF) + "," + + Integer.toHexString(buffer.get(offset + 2) & 0xFF) + "," + + Integer.toHexString(buffer.get(offset + 3) & 0xFF) + ")"); + } + + final int dataLen; + try (StreamInput input = buffer.streamInput()) { + input.skip(TcpHeader.MARKER_BYTES_SIZE); + dataLen = input.readInt(); + if (dataLen == PING_DATA_SIZE) { + // discard the messages we read and continue, this is achieved by skipping the bytes + // and returning null + return false; + } + } + + if (dataLen <= 0) { + throw new StreamCorruptedException("invalid data length: " + dataLen); + } + // safety against too large frames being sent + if (dataLen > NINETY_PER_HEAP_SIZE) { + throw new IllegalArgumentException("transport content length received [" + new ByteSizeValue(dataLen) + "] exceeded [" + + new ByteSizeValue(NINETY_PER_HEAP_SIZE) + "]"); + } + + if (buffer.length() < dataLen + sizeHeaderLength) { + throw new IllegalStateException("buffer must be >= to the message size but wasn't"); + } + return true; + } + + private static boolean bufferStartsWith(BytesReference buffer, int offset, String method) { + char[] chars = method.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (buffer.get(offset + i) != chars[i]) { + return false; + } + } + + return true; + } + + /** + * A helper exception to mark an incoming connection as potentially being HTTP + * so an appropriate error code can be returned + */ + public static class HttpOnTransportException extends ElasticsearchException { + + public HttpOnTransportException(String msg) { + super(msg); + } + + @Override + public RestStatus status() { + return RestStatus.BAD_REQUEST; + } + + public HttpOnTransportException(StreamInput in) throws IOException { + super(in); + } + } + + /** + * This method handles the message receive part for both request and responses + */ + public final void messageReceived(BytesReference reference, TcpChannel channel, String profileName, + InetSocketAddress remoteAddress, int messageLengthBytes) throws IOException { + final int totalMessageSize = messageLengthBytes + TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE; + readBytesMetric.inc(totalMessageSize); + // we have additional bytes to read, outside of the header + boolean hasMessageBytesToRead = (totalMessageSize - TcpHeader.HEADER_SIZE) > 0; + StreamInput streamIn = reference.streamInput(); + boolean success = false; + try (ThreadContext.StoredContext tCtx = threadPool.getThreadContext().stashContext()) { + long requestId = streamIn.readLong(); + byte status = streamIn.readByte(); + Version version = Version.fromId(streamIn.readInt()); + if (TransportStatus.isCompress(status) && hasMessageBytesToRead && streamIn.available() > 0) { + Compressor compressor; + try { + final int bytesConsumed = TcpHeader.REQUEST_ID_SIZE + TcpHeader.STATUS_SIZE + TcpHeader.VERSION_ID_SIZE; + compressor = CompressorFactory.compressor(reference.slice(bytesConsumed, reference.length() - bytesConsumed)); + } catch (NotCompressedException ex) { + int maxToRead = Math.min(reference.length(), 10); + StringBuilder sb = new StringBuilder("stream marked as compressed, but no compressor found, first [").append(maxToRead) + .append("] content bytes out of [").append(reference.length()) + .append("] readable bytes with message size [").append(messageLengthBytes).append("] ").append("] are ["); + for (int i = 0; i < maxToRead; i++) { + sb.append(reference.get(i)).append(","); + } + sb.append("]"); + throw new IllegalStateException(sb.toString()); + } + streamIn = compressor.streamInput(streamIn); + } + final boolean isHandshake = TransportStatus.isHandshake(status); + ensureVersionCompatibility(version, getCurrentVersion(), isHandshake); + streamIn = new NamedWriteableAwareStreamInput(streamIn, namedWriteableRegistry); + streamIn.setVersion(version); + threadPool.getThreadContext().readHeaders(streamIn); + if (TransportStatus.isRequest(status)) { + handleRequest(channel, profileName, streamIn, requestId, messageLengthBytes, version, remoteAddress, status); + } else { + final TransportResponseHandler handler; + if (isHandshake) { + handler = pendingHandshakes.remove(requestId); + } else { + TransportResponseHandler theHandler = transportService.onResponseReceived(requestId); + if (theHandler == null && TransportStatus.isError(status)) { + handler = pendingHandshakes.remove(requestId); + } else { + handler = theHandler; + } + } + // ignore if its null, the service logs it + if (handler != null) { + if (TransportStatus.isError(status)) { + handlerResponseError(streamIn, handler); + } else { + handleResponse(remoteAddress, streamIn, handler); + } + // Check the entire message has been read + final int nextByte = streamIn.read(); + // calling read() is useful to make sure the message is fully read, even if there is an EOS marker + if (nextByte != -1) { + throw new IllegalStateException("Message not fully read (response) for requestId [" + requestId + "], handler [" + + handler + "], error [" + TransportStatus.isError(status) + "]; resetting"); + } + } + } + success = true; + } finally { + if (success) { + IOUtils.close(streamIn); + } else { + IOUtils.closeWhileHandlingException(streamIn); + } + } + } + + static void ensureVersionCompatibility(Version version, Version currentVersion, boolean isHandshake) { + // for handshakes we are compatible with N-2 since otherwise we can't figure out our initial version + // since we are compatible with N-1 and N+1 so we always send our minCompatVersion as the initial version in the + // handshake. This looks odd but it's required to establish the connection correctly we check for real compatibility + // once the connection is established + final Version compatibilityVersion = isHandshake ? currentVersion.minimumCompatibilityVersion() : currentVersion; + if (version.isCompatible(compatibilityVersion) == false) { + final Version minCompatibilityVersion = isHandshake ? compatibilityVersion : compatibilityVersion.minimumCompatibilityVersion(); + String msg = "Received " + (isHandshake ? "handshake " : "") + "message from unsupported version: ["; + throw new IllegalStateException(msg + version + "] minimal compatible version is: [" + minCompatibilityVersion + "]"); + } + } + + private void handleResponse(InetSocketAddress remoteAddress, final StreamInput stream, final TransportResponseHandler handler) { + final TransportResponse response; + try { + response = handler.read(stream); + response.remoteAddress(new TransportAddress(remoteAddress)); + } catch (Exception e) { + handleException(handler, new TransportSerializationException( + "Failed to deserialize response from handler [" + handler.getClass().getName() + "]", e)); + return; + } + threadPool.executor(handler.executor()).execute(new AbstractRunnable() { + @Override + public void onFailure(Exception e) { + handleException(handler, new ResponseHandlerFailureTransportException(e)); + } + + @Override + protected void doRun() throws Exception { + handler.handleResponse(response); + } + }); + + } + + /** + * Executed for a received response error + */ + private void handlerResponseError(StreamInput stream, final TransportResponseHandler handler) { + Exception error; + try { + error = stream.readException(); + } catch (Exception e) { + error = new TransportSerializationException("Failed to deserialize exception response from stream", e); + } + handleException(handler, error); + } + + private void handleException(final TransportResponseHandler handler, Throwable error) { + if (!(error instanceof RemoteTransportException)) { + error = new RemoteTransportException(error.getMessage(), error); + } + final RemoteTransportException rtx = (RemoteTransportException) error; + threadPool.executor(handler.executor()).execute(() -> { + try { + handler.handleException(rtx); + } catch (Exception e) { + logger.error((Supplier) () -> new ParameterizedMessage("failed to handle exception response [{}]", handler), e); + } + }); + } + + protected String handleRequest(TcpChannel channel, String profileName, final StreamInput stream, long requestId, + int messageLengthBytes, Version version, InetSocketAddress remoteAddress, byte status) + throws IOException { + final String action = stream.readString(); + transportService.onRequestReceived(requestId, action); + TransportChannel transportChannel = null; + try { + if (TransportStatus.isHandshake(status)) { + final VersionHandshakeResponse response = new VersionHandshakeResponse(getCurrentVersion()); + sendResponse(version, channel, response, requestId, HANDSHAKE_ACTION_NAME, TransportResponseOptions.EMPTY, + TransportStatus.setHandshake((byte) 0)); + } else { + final RequestHandlerRegistry reg = transportService.getRequestHandler(action); + if (reg == null) { + throw new ActionNotFoundTransportException(action); + } + if (reg.canTripCircuitBreaker()) { + getInFlightRequestBreaker().addEstimateBytesAndMaybeBreak(messageLengthBytes, ""); + } else { + getInFlightRequestBreaker().addWithoutBreaking(messageLengthBytes); + } + transportChannel = new TcpTransportChannel(this, channel, transportName, action, requestId, version, profileName, + messageLengthBytes); + final TransportRequest request = reg.newRequest(stream); + request.remoteAddress(new TransportAddress(remoteAddress)); + // in case we throw an exception, i.e. when the limit is hit, we don't want to verify + validateRequest(stream, requestId, action); + threadPool.executor(reg.getExecutor()).execute(new RequestHandler(reg, request, transportChannel)); + } + } catch (Exception e) { + // the circuit breaker tripped + if (transportChannel == null) { + transportChannel = new TcpTransportChannel(this, channel, transportName, action, requestId, version, profileName, 0); + } + try { + transportChannel.sendResponse(e); + } catch (IOException inner) { + inner.addSuppressed(e); + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "Failed to send error message back to client for action [{}]", action), inner); + } + } + return action; + } + + // This template method is needed to inject custom error checking logic in tests. + protected void validateRequest(StreamInput stream, long requestId, String action) throws IOException { + final int nextByte = stream.read(); + // calling read() is useful to make sure the message is fully read, even if there some kind of EOS marker + if (nextByte != -1) { + throw new IllegalStateException("Message not fully read (request) for requestId [" + requestId + "], action [" + action + + "], available [" + stream.available() + "]; resetting"); + } + } + + class RequestHandler extends AbstractRunnable { + private final RequestHandlerRegistry reg; + private final TransportRequest request; + private final TransportChannel transportChannel; + + RequestHandler(RequestHandlerRegistry reg, TransportRequest request, TransportChannel transportChannel) { + this.reg = reg; + this.request = request; + this.transportChannel = transportChannel; + } + + @SuppressWarnings({"unchecked"}) + @Override + protected void doRun() throws Exception { + reg.processMessageReceived(request, transportChannel); + } + + @Override + public boolean isForceExecution() { + return reg.isForceExecution(); + } + + @Override + public void onFailure(Exception e) { + if (lifecycleState() == Lifecycle.State.STARTED) { + // we can only send a response transport is started.... + try { + transportChannel.sendResponse(e); + } catch (Exception inner) { + inner.addSuppressed(e); + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "Failed to send error message back to client for action [{}]", reg.getAction()), inner); + } + } + } + } + + private static final class VersionHandshakeResponse extends TransportResponse { + private Version version; + + private VersionHandshakeResponse(Version version) { + this.version = version; + } + + private VersionHandshakeResponse() { + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + version = Version.readVersion(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + assert version != null; + Version.writeVersion(version, out); + } + } + + protected Version executeHandshake(DiscoveryNode node, TcpChannel channel, TimeValue timeout) + throws IOException, InterruptedException { + numHandshakes.inc(); + final long requestId = newRequestId(); + final HandshakeResponseHandler handler = new HandshakeResponseHandler(channel); + AtomicReference versionRef = handler.versionRef; + AtomicReference exceptionRef = handler.exceptionRef; + pendingHandshakes.put(requestId, handler); + boolean success = false; + try { + if (channel.isOpen() == false) { + // we have to protect us here since sendRequestToChannel won't barf if the channel is closed. + // it's weird but to change it will cause a lot of impact on the exception handling code all over the codebase. + // yet, if we don't check the state here we might have registered a pending handshake handler but the close + // listener calling #onChannelClosed might have already run and we are waiting on the latch below unitl we time out. + throw new IllegalStateException("handshake failed, channel already closed"); + } + // for the request we use the minCompatVersion since we don't know what's the version of the node we talk to + // we also have no payload on the request but the response will contain the actual version of the node we talk + // to as the payload. + final Version minCompatVersion = getCurrentVersion().minimumCompatibilityVersion(); + sendRequestToChannel(node, channel, requestId, HANDSHAKE_ACTION_NAME, TransportRequest.Empty.INSTANCE, + TransportRequestOptions.EMPTY, minCompatVersion, TransportStatus.setHandshake((byte) 0)); + if (handler.latch.await(timeout.millis(), TimeUnit.MILLISECONDS) == false) { + throw new ConnectTransportException(node, "handshake_timeout[" + timeout + "]"); + } + success = true; + if (exceptionRef.get() != null) { + throw new IllegalStateException("handshake failed", exceptionRef.get()); + } else { + Version version = versionRef.get(); + if (getCurrentVersion().isCompatible(version) == false) { + throw new IllegalStateException("Received message from unsupported version: [" + version + + "] minimal compatible version is: [" + getCurrentVersion().minimumCompatibilityVersion() + "]"); + } + return version; + } + } finally { + final TransportResponseHandler removedHandler = pendingHandshakes.remove(requestId); + // in the case of a timeout or an exception on the send part the handshake has not been removed yet. + // but the timeout is tricky since it's basically a race condition so we only assert on the success case. + assert success && removedHandler == null || success == false : "handler for requestId [" + requestId + "] is not been removed"; + } + } + + final int getNumPendingHandshakes() { // for testing + return pendingHandshakes.size(); + } + + final long getNumHandshakes() { + return numHandshakes.count(); // for testing + } + + @Override + public long newRequestId() { + return requestIdGenerator.incrementAndGet(); + } + + /** + * Called once the channel is closed for instance due to a disconnect or a closed socket etc. + */ + private void cancelHandshakeForChannel(TcpChannel channel) { + final Optional first = pendingHandshakes.entrySet().stream() + .filter((entry) -> entry.getValue().channel == channel).map(Map.Entry::getKey).findFirst(); + if (first.isPresent()) { + final Long requestId = first.get(); + final HandshakeResponseHandler handler = pendingHandshakes.remove(requestId); + if (handler != null) { + // there might be a race removing this or this method might be called twice concurrently depending on how + // the channel is closed ie. due to connection reset or broken pipes + handler.handleException(new TransportException("connection reset")); + } + } + } + + /** + * Ensures this transport is still started / open + * + * @throws IllegalStateException if the transport is not started / open + */ + protected final void ensureOpen() { + if (lifecycle.started() == false) { + throw new IllegalStateException("transport has been stopped"); + } + } + + /** + * This listener increments the transmitted bytes metric on success. + */ + private abstract class SendMetricListener extends NotifyOnceListener { + private final long messageSize; + + private SendMetricListener(long messageSize) { + this.messageSize = messageSize; + } + + @Override + protected final void innerOnResponse(Void object) { + transmittedBytesMetric.inc(messageSize); + innerInnerOnResponse(object); + } + + protected abstract void innerInnerOnResponse(Void object); + } + + private final class SendListener extends SendMetricListener { + private final TcpChannel channel; + private final Closeable optionalCloseable; + private final Runnable transportAdaptorCallback; + + private SendListener(TcpChannel channel, Closeable optionalCloseable, Runnable transportAdaptorCallback, long messageLength) { + super(messageLength); + this.channel = channel; + this.optionalCloseable = optionalCloseable; + this.transportAdaptorCallback = transportAdaptorCallback; + } + + @Override + protected void innerInnerOnResponse(Void v) { + closeAndCallback(null); + } + + @Override + protected void innerOnFailure(Exception e) { + logger.warn(() -> new ParameterizedMessage("send message failed [channel: {}]", channel), e); + closeAndCallback(e); + } + + private void closeAndCallback(final Exception e) { + try { + IOUtils.close(optionalCloseable, transportAdaptorCallback::run); + } catch (final IOException inner) { + if (e != null) { + inner.addSuppressed(e); + } + throw new UncheckedIOException(inner); + } + } + } + + @Override + public final TransportStats getStats() { + return new TransportStats(acceptedChannels.size(), readBytesMetric.count(), readBytesMetric.sum(), transmittedBytesMetric.count(), + transmittedBytesMetric.sum()); + } + + /** + * Returns all profile settings for the given settings object + */ + public static Set getProfileSettings(Settings settings) { + HashSet profiles = new HashSet<>(); + boolean isDefaultSet = false; + for (String profile : settings.getGroups("transport.profiles.", true).keySet()) { + profiles.add(new ProfileSettings(settings, profile)); + if (DEFAULT_PROFILE.equals(profile)) { + isDefaultSet = true; + } + } + if (isDefaultSet == false) { + profiles.add(new ProfileSettings(settings, DEFAULT_PROFILE)); + } + return Collections.unmodifiableSet(profiles); + } + + /** + * Representation of a transport profile settings for a transport.profiles.$profilename.* + */ + public static final class ProfileSettings { + public final String profileName; + public final boolean tcpNoDelay; + public final boolean tcpKeepAlive; + public final boolean reuseAddress; + public final ByteSizeValue sendBufferSize; + public final ByteSizeValue receiveBufferSize; + public final List bindHosts; + public final List publishHosts; + public final String portOrRange; + public final int publishPort; + public final boolean isDefaultProfile; + + public ProfileSettings(Settings settings, String profileName) { + this.profileName = profileName; + isDefaultProfile = DEFAULT_PROFILE.equals(profileName); + tcpKeepAlive = TCP_KEEP_ALIVE_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + tcpNoDelay = TCP_NO_DELAY_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + reuseAddress = TCP_REUSE_ADDRESS_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + sendBufferSize = TCP_SEND_BUFFER_SIZE_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + receiveBufferSize = TCP_RECEIVE_BUFFER_SIZE_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + List profileBindHosts = BIND_HOST_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + bindHosts = (profileBindHosts.isEmpty() ? NetworkService.GLOBAL_NETWORK_BINDHOST_SETTING.get(settings) + : profileBindHosts); + publishHosts = PUBLISH_HOST_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + Setting concretePort = PORT_PROFILE.getConcreteSettingForNamespace(profileName); + if (concretePort.exists(settings) == false && isDefaultProfile == false) { + throw new IllegalStateException("profile [" + profileName + "] has no port configured"); + } + portOrRange = PORT_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + publishPort = isDefaultProfile ? PUBLISH_PORT.get(settings) : + PUBLISH_PORT_PROFILE.getConcreteSettingForNamespace(profileName).get(settings); + } + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransportChannel.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransportChannel.java new file mode 100644 index 0000000..b0e4695 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TcpTransportChannel.java @@ -0,0 +1,90 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.Version; +import org.elasticsearch.transport.TcpChannel; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseOptions; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class TcpTransportChannel implements TransportChannel { + private final TcpTransport transport; + private final Version version; + private final String action; + private final long requestId; + private final String profileName; + private final long reservedBytes; + private final AtomicBoolean released = new AtomicBoolean(); + private final String channelType; + private final TcpChannel channel; + + TcpTransportChannel(TcpTransport transport, TcpChannel channel, String channelType, String action, + long requestId, Version version, String profileName, long reservedBytes) { + this.version = version; + this.channel = channel; + this.transport = transport; + this.action = action; + this.requestId = requestId; + this.profileName = profileName; + this.reservedBytes = reservedBytes; + this.channelType = channelType; + } + + @Override + public String getProfileName() { + return profileName; + } + + @Override + public void sendResponse(TransportResponse response) throws IOException { + sendResponse(response, TransportResponseOptions.EMPTY); + } + + @Override + public void sendResponse(TransportResponse response, TransportResponseOptions options) throws IOException { + try { + transport.sendResponse(version, channel, response, requestId, action, options); + } finally { + release(false); + } + } + + @Override + public void sendResponse(Exception exception) throws IOException { + try { + transport.sendErrorResponse(version, channel, exception, requestId, action); + } finally { + release(true); + } + } + + private Exception releaseBy; + + private void release(boolean isExceptionResponse) { + if (released.compareAndSet(false, true)) { + assert (releaseBy = new Exception()) != null; // easier to debug if it's already closed + transport.getInFlightRequestBreaker().addWithoutBreaking(-reservedBytes); + } else if (isExceptionResponse == false) { + // only fail if we are not sending an error - we might send the error triggered by the previous + // sendResponse call + throw new IllegalStateException("reserved bytes are already released", releaseBy); + } + } + + @Override + public String getChannelType() { + return channelType; + } + + @Override + public Version getVersion() { + return version; + } + + public TcpChannel getChannel() { + return channel; + } +} + diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/Transport.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Transport.java new file mode 100644 index 0000000..95ca1e2 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/Transport.java @@ -0,0 +1,116 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.CheckedBiConsumer; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.breaker.NoopCircuitBreaker; +import org.elasticsearch.common.component.LifecycleComponent; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportStats; + +import java.io.Closeable; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Map; + +public interface Transport extends LifecycleComponent { + + Setting TRANSPORT_TCP_COMPRESS = Setting.boolSetting("transport.tcp.compress", false, Property.NodeScope); + + void setTransportService(TransportService service); + + /** + * The address the transport is bound on. + */ + BoundTransportAddress boundAddress(); + + /** + * Further profile bound addresses + * @return null iff profiles are unsupported, otherwise a map with name of profile and its bound transport address + */ + Map profileBoundAddresses(); + + /** + * Returns an address from its string representation. + */ + TransportAddress[] addressesFromString(String address, int perAddressLimit) throws UnknownHostException; + + /** + * Returns true if the node is connected. + */ + boolean nodeConnected(DiscoveryNode node); + + /** + * Connects to a node with the given connection profile. If the node is already connected this method has no effect. + * Once a successful is established, it can be validated before being exposed. + */ + void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile, + CheckedBiConsumer connectionValidator) throws ConnectTransportException; + + /** + * Disconnected from the given node, if not connected, will do nothing. + */ + void disconnectFromNode(DiscoveryNode node); + + List getLocalAddresses(); + + default CircuitBreaker getInFlightRequestBreaker() { + return new NoopCircuitBreaker("in-flight-noop"); + } + + /** + * Returns a new request ID to use when sending a message via {@link Connection#sendRequest(long, String, + * TransportRequest, TransportRequestOptions)} + */ + long newRequestId(); + + Connection getConnection(DiscoveryNode node); + + /** + * Opens a new connection to the given node and returns it. In contrast to + * {@link #connectToNode(DiscoveryNode, ConnectionProfile, CheckedBiConsumer)} the returned connection is not managed by + * the transport implementation. This connection must be closed once it's not needed anymore. + * This connection type can be used to execute a handshake between two nodes before the node will be published via + * {@link #connectToNode(DiscoveryNode, ConnectionProfile, CheckedBiConsumer)}. + */ + Connection openConnection(DiscoveryNode node, ConnectionProfile profile) throws IOException; + + TransportStats getStats(); + + /** + * A unidirectional connection to a {@link DiscoveryNode} + */ + interface Connection extends Closeable { + /** + * The node this connection is associated with + */ + DiscoveryNode getNode(); + + void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) throws + IOException, TransportException; + + /** + * Returns the version of the node this connection was established with. + */ + default Version getVersion() { + return getNode().getVersion(); + } + + /** + * Returns a key that this connection can be cached on. Delegating subclasses must delegate method call to + * the original connection. + */ + default Object getCacheKey() { + return this; + } + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportBulkClient.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportBulkClient.java new file mode 100644 index 0000000..a516429 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportBulkClient.java @@ -0,0 +1,129 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequestBuilder; +import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.xbib.elasticsearch.client.AbstractClient; +import org.xbib.elasticsearch.client.BulkControl; +import org.xbib.elasticsearch.client.BulkMetric; +import org.xbib.elasticsearch.client.NetworkUtils; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Transport client with additional methods for bulk processing. + */ +public class TransportBulkClient extends AbstractClient { + + private static final Logger logger = LogManager.getLogger(TransportBulkClient.class.getName()); + + public TransportBulkClient init(ElasticsearchClient client, Settings settings, BulkMetric metric, BulkControl control) { + super.init(client, settings, metric, control); + // auto-connect here + try { + Collection addrs = findAddresses(settings); + if (!connect(addrs, settings.getAsBoolean("autodiscover", false))) { + throw new NoNodeAvailableException("no cluster nodes available, check settings " + + settings.toString()); + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return this; + } + + protected ElasticsearchClient createClient(Settings settings) { + if (settings != null) { + String version = System.getProperty("os.name") + + " " + System.getProperty("java.vm.name") + + " " + System.getProperty("java.vm.vendor") + + " " + System.getProperty("java.runtime.version") + + " " + System.getProperty("java.vm.version"); + logger.info("creating transport client on {} with effective settings {}", + version, settings.toString()); + return new TransportClient(Settings.builder() + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), settings.get(ClusterName.CLUSTER_NAME_SETTING.getKey())) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), settings.get(EsExecutors.PROCESSORS_SETTING.getKey())) + .put("client.transport.ignore_cluster_name", true) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .build(), Collections.singletonList(Netty4Plugin.class)); + } + return null; + } + + @Override + public synchronized void shutdown() throws IOException { + super.shutdown(); + logger.info("shutting down..."); + if (client() != null) { + TransportClient client = (TransportClient) client(); + client.close(); + client.threadPool().shutdown(); + } + logger.info("shutting down completed"); + } + + private Collection findAddresses(Settings settings) throws IOException { + List hostnames = settings.getAsList("host", Collections.singletonList("localhost")); + int port = settings.getAsInt("port", 9300); + Collection addresses = new ArrayList<>(); + for (String hostname : hostnames) { + String[] splitHost = hostname.split(":", 2); + if (splitHost.length == 2) { + String host = splitHost[0]; + InetAddress inetAddress = NetworkUtils.resolveInetAddress(host, null); + try { + port = Integer.parseInt(splitHost[1]); + } catch (Exception e) { + logger.warn(e.getMessage(), e); + } + addresses.add(new TransportAddress(inetAddress, port)); + } + if (splitHost.length == 1) { + String host = splitHost[0]; + InetAddress inetAddress = NetworkUtils.resolveInetAddress(host, null); + addresses.add(new TransportAddress(inetAddress, port)); + } + } + return addresses; + } + + private boolean connect(Collection addresses, boolean autodiscover) { + logger.info("trying to connect to {}", addresses); + if (client() == null) { + throw new IllegalStateException("no client?"); + } + TransportClient transportClient = (TransportClient) client(); + transportClient.addTransportAddresses(addresses); + List nodes = transportClient.connectedNodes(); + logger.info("nodes = {}", nodes); + if (nodes != null && !nodes.isEmpty()) { + if (autodiscover) { + logger.info("trying to auto-discover all cluster nodes..."); + ClusterStateRequestBuilder clusterStateRequestBuilder = + new ClusterStateRequestBuilder(client(), ClusterStateAction.INSTANCE); + ClusterStateResponse clusterStateResponse = clusterStateRequestBuilder.execute().actionGet(); + DiscoveryNodes discoveryNodes = clusterStateResponse.getState().getNodes(); + transportClient.addDiscoveryNodes(discoveryNodes); + logger.info("after auto-discovery connected to {}", transportClient.connectedNodes()); + } + return true; + } + return false; + } +} diff --git a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportClient.java similarity index 75% rename from src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java rename to transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportClient.java index 6707982..f7b79cd 100644 --- a/src/main/java/org/xbib/elasticsearch/extras/client/transport/TransportClient.java +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportClient.java @@ -1,4 +1,4 @@ -package org.xbib.elasticsearch.extras.client.transport; +package org.xbib.elasticsearch.client.transport; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; import static java.util.stream.Collectors.toList; @@ -9,12 +9,12 @@ import org.apache.lucene.util.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionListenerResponseHandler; import org.elasticsearch.action.ActionModule; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.action.GenericAction; -import org.elasticsearch.action.TransportActionNodeProxy; import org.elasticsearch.action.admin.cluster.node.liveness.LivenessRequest; import org.elasticsearch.action.admin.cluster.node.liveness.LivenessResponse; import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction; @@ -24,26 +24,23 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Injector; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.ModulesBuilder; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; -import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.node.InternalSettingsPreparer; import org.elasticsearch.node.Node; import org.elasticsearch.plugins.ActionPlugin; -import org.elasticsearch.plugins.NetworkPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.plugins.SearchPlugin; @@ -51,12 +48,10 @@ import org.elasticsearch.search.SearchModule; import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.FutureTransportResponseHandler; -import org.elasticsearch.transport.TcpTransport; -import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportRequestOptions; -import org.elasticsearch.transport.TransportService; import java.io.Closeable; +import java.io.IOException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; @@ -64,18 +59,16 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; /** - * Stripped-down transport client without node sampling and without retrying. + * Simplified transport client, without the node sampling and retrying mode like in the mainline version. * - * Merged together: original TransportClient, TransportClientNodesServce, TransportClientProxy - * Configurable connect ping interval setting added. */ public class TransportClient extends AbstractClient { @@ -92,8 +85,6 @@ public class TransportClient extends AbstractClient { private final TransportService transportService; - private final ProxyActionMap proxy; - private final AtomicInteger tempNodeId = new AtomicInteger(); private final AtomicInteger nodeCounter = new AtomicInteger(); @@ -113,7 +104,7 @@ public class TransportClient extends AbstractClient { * @param settings settings */ public TransportClient(Settings settings) { - this(buildTemplate(settings, Settings.EMPTY, Collections.emptyList())); + this(buildParams(settings, Settings.EMPTY, Collections.emptyList())); } /** @@ -122,7 +113,7 @@ public class TransportClient extends AbstractClient { * @param plugins plugins */ public TransportClient(Settings settings, Collection> plugins) { - this(buildTemplate(settings, Settings.EMPTY, plugins)); + this(buildParams(settings, Settings.EMPTY, plugins)); } /** @@ -132,16 +123,23 @@ public class TransportClient extends AbstractClient { * @param plugins the client plugins */ protected TransportClient(Settings settings, Settings defaultSettings, Collection> plugins) { - this(buildTemplate(settings, defaultSettings, plugins)); + this(buildParams(settings, defaultSettings, plugins)); } - private TransportClient(ClientTemplate template) { - super(template.getSettings(), template.getThreadPool()); - this.injector = template.injector; - this.clusterName = new ClusterName(template.getSettings().get("cluster.name", "elasticsearch")); + private TransportClient(final Injector injector) { + super(getSettings(injector), getThreadPool(injector)); + this.injector = injector; + this.clusterName = new ClusterName(getSettings(injector).get("cluster.name", "elasticsearch")); this.transportService = injector.getInstance(TransportService.class); this.pingTimeout = this.settings.getAsTime("client.transport.ping_timeout", timeValueSeconds(5)).millis(); - this.proxy = template.proxy; + } + + private static Settings getSettings(Injector injector) { + return injector.getInstance(Settings.class); + } + + private static ThreadPool getThreadPool(Injector injector) { + return injector.getInstance(ThreadPool.class); } /** @@ -150,11 +148,11 @@ public class TransportClient extends AbstractClient { * @return list of transport addresess */ public List transportAddresses() { - List lstBuilder = new ArrayList<>(); + List list = new ArrayList<>(); for (DiscoveryNode listedNode : listedNodes) { - lstBuilder.add(listedNode.getAddress()); + list.add(listedNode.getAddress()); } - return Collections.unmodifiableList(lstBuilder); + return Collections.unmodifiableList(list); } /** @@ -169,8 +167,7 @@ public class TransportClient extends AbstractClient { } /** - * The list of filtered nodes that were not connected to, for example, due to - * mismatch in cluster name. + * The list of filtered nodes that were not connected to, for example, due to mismatch in cluster name. * * @return list of nodes */ @@ -179,7 +176,7 @@ public class TransportClient extends AbstractClient { } /** - * Returns the listed nodes in the transport client (ones added to it). + * Returns the listed nodes in the transport client, once added to it. * * @return list of nodes */ @@ -197,9 +194,9 @@ public class TransportClient extends AbstractClient { * @return this transport client */ public TransportClient addDiscoveryNodes(DiscoveryNodes discoveryNodes) { - Collection addresses = new ArrayList<>(); + Collection addresses = new ArrayList<>(); for (DiscoveryNode discoveryNode : discoveryNodes) { - addresses.add((InetSocketTransportAddress) discoveryNode.getAddress()); + addresses.add(discoveryNode.getAddress()); } addTransportAddresses(addresses); return this; @@ -211,24 +208,22 @@ public class TransportClient extends AbstractClient { * If it is unavailable, it will be automatically connected to once it is up. * In order to get the list of all the current connected nodes, please see {@link #connectedNodes()}. * - * @param transportAddresses transport addressses - * @return this transport client + * @param transportAddresses transport addresses */ - public TransportClient addTransportAddresses(Collection transportAddresses) { + public TransportClient addTransportAddresses(Collection transportAddresses) { synchronized (mutex) { if (closed) { throw new IllegalStateException("transport client is closed, can't add addresses"); } - Set discoveryNodeList = new HashSet<>(); - discoveryNodeList.addAll(listedNodes); - logger.debug("before adding: nodes={} listednodes={} transportAddresses={}", + Set discoveryNodeList = new HashSet<>(listedNodes); + logger.info("before adding: nodes={} listednodes={} transportAddresses={}", nodes, listedNodes, transportAddresses); for (TransportAddress newTransportAddress : transportAddresses) { boolean found = false; for (DiscoveryNode discoveryNode : discoveryNodeList) { logger.debug("checking existing address [{}] against new [{}]", discoveryNode.getAddress(), newTransportAddress); - if (discoveryNode.getAddress().sameHost(newTransportAddress)) { + if (discoveryNode.getAddress().equals(newTransportAddress)) { // sameHost found = true; logger.debug("address [{}] already connected, ignoring", newTransportAddress, discoveryNode); break; @@ -236,9 +231,8 @@ public class TransportClient extends AbstractClient { } if (!found) { DiscoveryNode node = new DiscoveryNode("#transport#-" + tempNodeId.incrementAndGet(), - newTransportAddress, - Version.CURRENT.minimumCompatibilityVersion()); - logger.debug("adding address [{}]", node); + newTransportAddress, Version.CURRENT.minimumCompatibilityVersion()); + logger.info("adding new address [{}]", node); discoveryNodeList.add(node); } } @@ -280,25 +274,36 @@ public class TransportClient extends AbstractClient { return; } closed = true; - logger.debug("disconnecting from nodes {}", nodes); + logger.info("disconnecting from nodes {}", nodes); for (DiscoveryNode node : nodes) { transportService.disconnectFromNode(node); } nodes = Collections.emptyList(); - logger.debug("disconnecting from listed nodes {}", listedNodes); + logger.info("disconnecting from listed nodes {}", listedNodes); for (DiscoveryNode listedNode : listedNodes) { transportService.disconnectFromNode(listedNode); } listedNodes = Collections.emptyList(); } - injector.getInstance(TransportService.class).close(); - for (Class plugin : injector.getInstance(PluginsService.class).getGuiceServiceClasses()) { - injector.getInstance(plugin).close(); + transportService.close(); + PluginsService pluginsService = injector.getInstance(PluginsService.class); + for (Class guiceService : pluginsService.getGuiceServiceClasses()) { + logger.info("closing plugin service {}", guiceService); + injector.getInstance(guiceService).close(); } + // closing all plugins + pluginsService.filterPlugins(Plugin.class).forEach(plugin -> { + try { + logger.info("closing plugin {}", plugin); + plugin.close(); + } catch (IOException e) { + logger.warn(e.getMessage(), e); + } + }); try { ThreadPool.terminate(injector.getInstance(ThreadPool.class), 10, TimeUnit.SECONDS); } catch (Exception e) { - logger.debug(e.getMessage(), e); + logger.warn(e.getMessage(), e); } } @@ -308,10 +313,10 @@ public class TransportClient extends AbstractClient { for (DiscoveryNode listedNode : listedNodes) { if (!transportService.nodeConnected(listedNode)) { try { - logger.debug("connecting to listed node [{}]", listedNode); + logger.info("connecting to listed node " + listedNode); transportService.connectToNode(listedNode); } catch (Exception e) { - logger.debug("failed to connect to node [{}]", e); + logger.warn("failed to connect to node " + listedNode, e); continue; } } @@ -321,6 +326,7 @@ public class TransportClient extends AbstractClient { TransportRequestOptions.builder().withType(TransportRequestOptions.Type.STATE) .withTimeout(pingTimeout).build(), new FutureTransportResponseHandler() { + @SuppressWarnings("deprecation") @Override public LivenessResponse newInstance() { return new LivenessResponse(); @@ -341,7 +347,7 @@ public class TransportClient extends AbstractClient { newNodes.add(listedNode); } } catch (Exception e) { - logger.info("failed to get node info for {}, disconnecting", e, listedNode); + logger.warn("failed to get node info for {}, disconnecting", e, listedNode); transportService.disconnectFromNode(listedNode); } } @@ -353,12 +359,12 @@ public class TransportClient extends AbstractClient { transportService.connectToNode(node); } catch (Exception e) { it.remove(); - logger.debug("failed to connect to new node [" + node + "], removed", e); + logger.warn("failed to connect to new node [" + node + "], removed", e); } } } this.nodes = Collections.unmodifiableList(new ArrayList<>(newNodes)); - logger.debug("connected to {} nodes", nodes.size()); + logger.info("connected to nodes: {}", nodes); this.filteredNodes = Collections.unmodifiableList(new ArrayList<>(newFilteredNodes)); } @@ -366,10 +372,6 @@ public class TransportClient extends AbstractClient { @SuppressWarnings({"unchecked", "rawtypes"}) protected > void doExecute(Action action, final R request, final ActionListener listener) { - final TransportActionNodeProxy proxyAction = proxy.getProxies().get(action); - if (proxyAction == null) { - throw new IllegalStateException("undefined action " + action); - } List nodeList = this.nodes; if (nodeList.isEmpty()) { throw new NoNodeAvailableException("none of the configured nodes are available: " + this.listedNodes); @@ -379,40 +381,24 @@ public class TransportClient extends AbstractClient { index = 0; nodeCounter.set(0); } + DiscoveryNode discoveryNode = nodeList.get(index % nodeList.size()); // try once and never more try { - proxyAction.execute(nodeList.get(index % nodeList.size()), request, listener); + ActionRequestValidationException validationException = request.validate(); + if (validationException != null) { + listener.onFailure(validationException); + return; + } + TransportRequestOptions transportOptions = action.transportOptions(settings); + transportService.sendRequest(discoveryNode, action.name(), request, transportOptions, + new ActionListenerResponseHandler<>(listener, action::newResponse)); } catch (Exception e) { listener.onFailure(e); } } - /** - * The {@link ProxyActionMap} must be declared public. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private static class ProxyActionMap { - - private final Map proxies; - - public ProxyActionMap(Settings settings, TransportService transportService, List actions) { - MapBuilder actionsBuilder = new MapBuilder<>(); - for (GenericAction action : actions) { - if (action instanceof Action) { - actionsBuilder.put((Action) action, new TransportActionNodeProxy(settings, action, transportService)); - } - } - this.proxies = actionsBuilder.immutableMap(); - } - - Map getProxies() { - return proxies; - } - } - - - private static ClientTemplate buildTemplate(Settings givenSettings, Settings defaultSettings, - Collection> plugins) { + private static Injector buildParams(Settings givenSettings, Settings defaultSettings, + Collection> plugins) { Settings providedSettings = givenSettings; if (!Node.NODE_NAME_SETTING.exists(providedSettings)) { providedSettings = Settings.builder().put(providedSettings) @@ -424,17 +410,14 @@ public class TransportClient extends AbstractClient { final List resourcesToClose = new ArrayList<>(); final ThreadPool threadPool = new ThreadPool(settings); resourcesToClose.add(() -> ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS)); - final NetworkService networkService = new NetworkService(settings, Collections.emptyList()); + final NetworkService networkService = new NetworkService(Collections.emptyList()); try { - final List> additionalSettings = new ArrayList<>(); - final List additionalSettingsFilter = new ArrayList<>(); - additionalSettings.addAll(pluginsService.getPluginSettings()); - additionalSettingsFilter.addAll(pluginsService.getPluginSettingsFilter()); + final List> additionalSettings = new ArrayList<>(pluginsService.getPluginSettings()); + final List additionalSettingsFilter = new ArrayList<>(pluginsService.getPluginSettingsFilter()); for (final ExecutorBuilder builder : threadPool.builders()) { additionalSettings.addAll(builder.getRegisteredSettings()); } SettingsModule settingsModule = new SettingsModule(settings, additionalSettings, additionalSettingsFilter); - SearchModule searchModule = new SearchModule(settings, true, pluginsService.filterPlugins(SearchPlugin.class)); List entries = new ArrayList<>(); @@ -460,23 +443,24 @@ public class TransportClient extends AbstractClient { settingsModule.getClusterSettings(), settingsModule.getSettingsFilter(), threadPool, - pluginsService.filterPlugins(ActionPlugin.class), null, null); + pluginsService.filterPlugins(ActionPlugin.class), null, null, null); modules.add(actionModule); CircuitBreakerService circuitBreakerService = Node.createCircuitBreakerService(settingsModule.getSettings(), settingsModule.getClusterSettings()); - BigArrays bigArrays = new BigArrays(settings, circuitBreakerService); + PageCacheRecycler pageCacheRecycler = new PageCacheRecycler(settings); + BigArrays bigArrays = new BigArrays(pageCacheRecycler, circuitBreakerService); resourcesToClose.add(circuitBreakerService); resourcesToClose.add(bigArrays); modules.add(settingsModule); NetworkModule networkModule = new NetworkModule(settings, true, pluginsService.filterPlugins(NetworkPlugin.class), threadPool, - bigArrays, circuitBreakerService, namedWriteableRegistry, + bigArrays, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, xContentRegistry, networkService, null); final Transport transport = networkModule.getTransportSupplier().get(); final TransportService transportService = new TransportService(settings, transport, threadPool, - networkModule.getTransportInterceptor(), boundTransportAddress -> - DiscoveryNode.createLocal(settings, dummyAddress(networkModule), UUIDs.randomBase64UUID()), - null); + networkModule.getTransportInterceptor(), + boundTransportAddress -> DiscoveryNode.createLocal(settings, new TransportAddress(TransportAddress.META_ADDRESS, 0), + UUIDs.randomBase64UUID()), null, Collections.emptySet()); modules.add((b -> { b.bind(BigArrays.class).toInstance(bigArrays); b.bind(PluginsService.class).toInstance(pluginsService); @@ -487,18 +471,13 @@ public class TransportClient extends AbstractClient { b.bind(NetworkService.class).toInstance(networkService); })); Injector injector = modules.createInjector(); - final ProxyActionMap proxy = new ProxyActionMap(settings, transportService, - actionModule.getActions().values().stream() - .map(ActionPlugin.ActionHandler::getAction).collect(toList())); - List pluginLifecycleComponents = new ArrayList<>(); - pluginLifecycleComponents.addAll(pluginsService.getGuiceServiceClasses().stream() - .map(injector::getInstance).collect(toList())); + List pluginLifecycleComponents = pluginsService.getGuiceServiceClasses() + .stream().map(injector::getInstance).collect(Collectors.toList()); resourcesToClose.addAll(pluginLifecycleComponents); transportService.start(); transportService.acceptIncomingRequests(); - ClientTemplate transportClient = new ClientTemplate(injector, proxy); resourcesToClose.clear(); - return transportClient; + return injector; } finally { IOUtils.closeWhileHandlingException(resourcesToClose); } @@ -520,28 +499,9 @@ public class TransportClient extends AbstractClient { .put(NetworkService.NETWORK_SERVER.getKey(), false) .put(CLIENT_TYPE_SETTING_S.getKey(), CLIENT_TYPE); if (!settings.isEmpty()) { - logger.info(settings.getAsMap()); + logger.info(settings.toString()); settingsBuilder.put(InternalSettingsPreparer.prepareSettings(settings)); } - return new PluginsService(settingsBuilder.build(), null, null, plugins); - } - - private static final class ClientTemplate { - final Injector injector; - private final ProxyActionMap proxy; - - private ClientTemplate(Injector injector, - ProxyActionMap proxy) { - this.injector = injector; - this.proxy = proxy; - } - - Settings getSettings() { - return injector.getInstance(Settings.class); - } - - ThreadPool getThreadPool() { - return injector.getInstance(ThreadPool.class); - } + return new PluginsService(settingsBuilder.build(), null, null, null, plugins); } } diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportConnectionListener.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportConnectionListener.java new file mode 100644 index 0000000..db349a5 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportConnectionListener.java @@ -0,0 +1,27 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.cluster.node.DiscoveryNode; + +public interface TransportConnectionListener { + + /** + * Called once a node connection is opened and registered. + */ + default void onNodeConnected(DiscoveryNode node) {} + + /** + * Called once a node connection is closed and unregistered. + */ + default void onNodeDisconnected(DiscoveryNode node) {} + + /** + * Called once a node connection is closed. The connection might not have been registered in the + * transport as a shared connection to a specific node + */ + default void onConnectionClosed(Transport.Connection connection) {} + + /** + * Called once a node connection is opened. + */ + default void onConnectionOpened(Transport.Connection connection) {} +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportInterceptor.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportInterceptor.java new file mode 100644 index 0000000..6235693 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportInterceptor.java @@ -0,0 +1,31 @@ +package org.xbib.elasticsearch.client.transport; + +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; + +/** + * This interface allows plugins to intercept requests on both the sender and the receiver side. + */ +public interface TransportInterceptor { + + default TransportRequestHandler interceptHandler(String action, String executor, + boolean forceExecution, + TransportRequestHandler actualHandler) { + return actualHandler; + } + + + default AsyncSender interceptSender(AsyncSender sender) { + return sender; + } + + + interface AsyncSender { + void sendRequest(Transport.Connection connection, String action, + TransportRequest request, TransportRequestOptions options, + TransportResponseHandler handler); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportService.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportService.java new file mode 100644 index 0000000..d035f85 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportService.java @@ -0,0 +1,1224 @@ +package org.xbib.elasticsearch.client.transport; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.ConcurrentMapLong; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; +import org.elasticsearch.common.util.concurrent.FutureUtils; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskCancelledException; +import org.elasticsearch.tasks.TaskManager; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.ActionNotFoundTransportException; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.FutureTransportResponseHandler; +import org.elasticsearch.transport.NodeDisconnectedException; +import org.elasticsearch.transport.NodeNotConnectedException; +import org.elasticsearch.transport.PlainTransportFuture; +import org.elasticsearch.transport.ReceiveTimeoutTransportException; +import org.elasticsearch.transport.RemoteTransportException; +import org.elasticsearch.transport.RequestHandlerRegistry; +import org.elasticsearch.transport.ResponseHandlerFailureTransportException; +import org.elasticsearch.transport.SendRequestTransportException; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportFuture; +import org.elasticsearch.transport.TransportInfo; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportResponseOptions; +import org.elasticsearch.transport.TransportStats; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static org.elasticsearch.common.settings.Setting.listSetting; + +public class TransportService extends AbstractLifecycleComponent { + + public static final String DIRECT_RESPONSE_PROFILE = ".direct"; + public static final String HANDSHAKE_ACTION_NAME = "internal:transport/handshake"; + + private final CountDownLatch blockIncomingRequestsLatch = new CountDownLatch(1); + protected final Transport transport; + protected final ThreadPool threadPool; + protected final ClusterName clusterName; + protected final TaskManager taskManager; + private final TransportInterceptor.AsyncSender asyncSender; + private final Function localNodeFactory; + private final boolean connectToRemoteCluster; + + volatile Map requestHandlers = Collections.emptyMap(); + final Object requestHandlerMutex = new Object(); + + final ConcurrentMapLong clientHandlers = ConcurrentCollections.newConcurrentMapLongWithAggressiveConcurrency(); + + final CopyOnWriteArrayList connectionListeners = new CopyOnWriteArrayList<>(); + + private final TransportInterceptor interceptor; + + // An LRU (don't really care about concurrency here) that holds the latest timed out requests so if they + // do show up, we can print more descriptive information about them + final Map timeoutInfoHandlers = + Collections.synchronizedMap(new LinkedHashMap(100, .75F, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 100; + } + }); + + public static final TransportInterceptor NOOP_TRANSPORT_INTERCEPTOR = new TransportInterceptor() {}; + + // tracer log + + public static final Setting> TRACE_LOG_INCLUDE_SETTING = + listSetting("transport.tracer.include", emptyList(), Function.identity(), Property.Dynamic, Property.NodeScope); + public static final Setting> TRACE_LOG_EXCLUDE_SETTING = + listSetting("transport.tracer.exclude", Arrays.asList("internal:discovery/zen/fd*", TransportLivenessAction.NAME), + Function.identity(), Property.Dynamic, Property.NodeScope); + + private final Logger tracerLog; + + volatile String[] tracerLogInclude; + volatile String[] tracerLogExclude; + + private final RemoteClusterService remoteClusterService; + + /** if set will call requests sent to this id to shortcut and executed locally */ + volatile DiscoveryNode localNode = null; + private final Transport.Connection localNodeConnection = new Transport.Connection() { + @Override + public DiscoveryNode getNode() { + return localNode; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + sendLocalRequest(requestId, action, request, options); + } + + @Override + public void close() throws IOException { + } + }; + + /** + * Build the service. + * + * @param clusterSettings if non null, the {@linkplain TransportService} will register with the {@link ClusterSettings} for settings + * updates for {@link #TRACE_LOG_EXCLUDE_SETTING} and {@link #TRACE_LOG_INCLUDE_SETTING}. + */ + public TransportService(Settings settings, Transport transport, ThreadPool threadPool, TransportInterceptor transportInterceptor, + Function localNodeFactory, @Nullable ClusterSettings clusterSettings, + Set taskHeaders) { + super(settings); + this.transport = transport; + this.threadPool = threadPool; + this.localNodeFactory = localNodeFactory; + this.clusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); + setTracerLogInclude(TRACE_LOG_INCLUDE_SETTING.get(settings)); + setTracerLogExclude(TRACE_LOG_EXCLUDE_SETTING.get(settings)); + tracerLog = Loggers.getLogger(logger, ".tracer"); + taskManager = createTaskManager(settings, threadPool, taskHeaders); + this.interceptor = transportInterceptor; + this.asyncSender = interceptor.interceptSender(this::sendRequestInternal); + this.connectToRemoteCluster = RemoteClusterService.ENABLE_REMOTE_CLUSTERS.get(settings); + remoteClusterService = new RemoteClusterService(settings, this); + if (clusterSettings != null) { + clusterSettings.addSettingsUpdateConsumer(TRACE_LOG_INCLUDE_SETTING, this::setTracerLogInclude); + clusterSettings.addSettingsUpdateConsumer(TRACE_LOG_EXCLUDE_SETTING, this::setTracerLogExclude); + if (connectToRemoteCluster) { + remoteClusterService.listenForUpdates(clusterSettings); + } + } + } + + public RemoteClusterService getRemoteClusterService() { + return remoteClusterService; + } + + public DiscoveryNode getLocalNode() { + return localNode; + } + + public TaskManager getTaskManager() { + return taskManager; + } + + protected TaskManager createTaskManager(Settings settings, ThreadPool threadPool, Set taskHeaders) { + return new TaskManager(settings, threadPool, taskHeaders); + } + + /** + * The executor service for this transport service. + * + * @return the executor service + */ + protected ExecutorService getExecutorService() { + return threadPool.generic(); + } + + void setTracerLogInclude(List tracerLogInclude) { + this.tracerLogInclude = tracerLogInclude.toArray(Strings.EMPTY_ARRAY); + } + + void setTracerLogExclude(List tracerLogExclude) { + this.tracerLogExclude = tracerLogExclude.toArray(Strings.EMPTY_ARRAY); + } + + @Override + protected void doStart() { + transport.setTransportService(this); + transport.start(); + + if (transport.boundAddress() != null && logger.isInfoEnabled()) { + logger.info("{}", transport.boundAddress()); + for (Map.Entry entry : transport.profileBoundAddresses().entrySet()) { + logger.info("profile [{}]: {}", entry.getKey(), entry.getValue()); + } + } + localNode = localNodeFactory.apply(transport.boundAddress()); + registerRequestHandler( + HANDSHAKE_ACTION_NAME, + () -> HandshakeRequest.INSTANCE, + ThreadPool.Names.SAME, + false, false, + (request, channel) -> channel.sendResponse( + new HandshakeResponse(localNode, clusterName, localNode.getVersion()))); + if (connectToRemoteCluster) { + // here we start to connect to the remote clusters + remoteClusterService.initializeRemoteClusters(); + } + } + + @Override + protected void doStop() { + try { + transport.stop(); + } finally { + // in case the transport is not connected to our local node (thus cleaned on node disconnect) + // make sure to clean any leftover on going handles + for (Map.Entry entry : clientHandlers.entrySet()) { + final RequestHolder holderToNotify = clientHandlers.remove(entry.getKey()); + if (holderToNotify != null) { + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + getExecutorService().execute(new AbstractRunnable() { + @Override + public void onRejection(Exception e) { + // if we get rejected during node shutdown we don't wanna bubble it up + logger.debug( + (Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on rejection, action: {}", + holderToNotify.action()), + e); + } + @Override + public void onFailure(Exception e) { + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on exception, action: {}", + holderToNotify.action()), + e); + } + @Override + public void doRun() { + TransportException ex = new TransportException("transport stopped, action: " + holderToNotify.action()); + holderToNotify.handler().handleException(ex); + } + }); + } + } + } + } + + @Override + protected void doClose() throws IOException { + IOUtils.close(remoteClusterService, transport); + } + + /** + * start accepting incoming requests. + * when the transport layer starts up it will block any incoming requests until + * this method is called + */ + public final void acceptIncomingRequests() { + blockIncomingRequestsLatch.countDown(); + } + + public TransportInfo info() { + BoundTransportAddress boundTransportAddress = boundAddress(); + if (boundTransportAddress == null) { + return null; + } + return new TransportInfo(boundTransportAddress, transport.profileBoundAddresses()); + } + + public TransportStats stats() { + return transport.getStats(); + } + + public BoundTransportAddress boundAddress() { + return transport.boundAddress(); + } + + public List getLocalAddresses() { + return transport.getLocalAddresses(); + } + + /** + * Returns true iff the given node is already connected. + */ + public boolean nodeConnected(DiscoveryNode node) { + return isLocalNode(node) || transport.nodeConnected(node); + } + + public void connectToNode(DiscoveryNode node) throws ConnectTransportException { + connectToNode(node, null); + } + + /** + * Connect to the specified node with the given connection profile + * + * @param node the node to connect to + * @param connectionProfile the connection profile to use when connecting to this node + */ + public void connectToNode(final DiscoveryNode node, ConnectionProfile connectionProfile) { + if (isLocalNode(node)) { + return; + } + transport.connectToNode(node, connectionProfile, (newConnection, actualProfile) -> { + // We don't validate cluster names to allow for tribe node connections. + final DiscoveryNode remote = handshake(newConnection, actualProfile.getHandshakeTimeout().millis(), cn -> true); + // removed for TransportClient + //if (node.equals(remote) == false) { + // throw new ConnectTransportException(node, "handshake failed. unexpected remote node " + remote); + //} + }); + } + + /** + * Establishes and returns a new connection to the given node. The connection is NOT maintained by this service, it's the callers + * responsibility to close the connection once it goes out of scope. + * @param node the node to connect to + * @param profile the connection profile to use + */ + public Transport.Connection openConnection(final DiscoveryNode node, ConnectionProfile profile) throws IOException { + if (isLocalNode(node)) { + return localNodeConnection; + } else { + return transport.openConnection(node, profile); + } + } + + /** + * Executes a high-level handshake using the given connection + * and returns the discovery node of the node the connection + * was established with. The handshake will fail if the cluster + * name on the target node mismatches the local cluster name. + * + * @param connection the connection to a specific node + * @param handshakeTimeout handshake timeout + * @return the connected node + * @throws ConnectTransportException if the connection failed + * @throws IllegalStateException if the handshake failed + */ + public DiscoveryNode handshake( + final Transport.Connection connection, + final long handshakeTimeout) throws ConnectTransportException { + return handshake(connection, handshakeTimeout, clusterName::equals); + } + + /** + * Executes a high-level handshake using the given connection + * and returns the discovery node of the node the connection + * was established with. The handshake will fail if the cluster + * name on the target node doesn't match the local cluster name. + * + * @param connection the connection to a specific node + * @param handshakeTimeout handshake timeout + * @param clusterNamePredicate cluster name validation predicate + * @return the connected node + * @throws ConnectTransportException if the connection failed + * @throws IllegalStateException if the handshake failed + */ + public DiscoveryNode handshake( + final Transport.Connection connection, + final long handshakeTimeout, Predicate clusterNamePredicate) throws ConnectTransportException { + final HandshakeResponse response; + final DiscoveryNode node = connection.getNode(); + try { + PlainTransportFuture futureHandler = new PlainTransportFuture<>( + new FutureTransportResponseHandler() { + @Override + public HandshakeResponse newInstance() { + return new HandshakeResponse(); + } + }); + sendRequest(connection, HANDSHAKE_ACTION_NAME, HandshakeRequest.INSTANCE, + TransportRequestOptions.builder().withTimeout(handshakeTimeout).build(), futureHandler); + response = futureHandler.txGet(); + } catch (Exception e) { + throw new IllegalStateException("handshake failed with " + node, e); + } + + if (!clusterNamePredicate.test(response.clusterName)) { + throw new IllegalStateException("handshake failed, mismatched cluster name [" + response.clusterName + "] - " + node); + } else if (response.version.isCompatible(localNode.getVersion()) == false) { + throw new IllegalStateException("handshake failed, incompatible version [" + response.version + "] - " + node); + } + logger.info("handshake: success with node {}", response.discoveryNode); + return response.discoveryNode; + } + + static class HandshakeRequest extends TransportRequest { + + public static final HandshakeRequest INSTANCE = new HandshakeRequest(); + + private HandshakeRequest() { + } + + } + + public static class HandshakeResponse extends TransportResponse { + private DiscoveryNode discoveryNode; + private ClusterName clusterName; + private Version version; + + HandshakeResponse() { + } + + public HandshakeResponse(DiscoveryNode discoveryNode, ClusterName clusterName, Version version) { + this.discoveryNode = discoveryNode; + this.version = version; + this.clusterName = clusterName; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + discoveryNode = in.readOptionalWriteable(DiscoveryNode::new); + clusterName = new ClusterName(in); + version = Version.readVersion(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalWriteable(discoveryNode); + clusterName.writeTo(out); + Version.writeVersion(version, out); + } + } + + public void disconnectFromNode(DiscoveryNode node) { + if (isLocalNode(node)) { + return; + } + transport.disconnectFromNode(node); + } + + public void addConnectionListener(TransportConnectionListener listener) { + connectionListeners.add(listener); + } + + public void removeConnectionListener(TransportConnectionListener listener) { + connectionListeners.remove(listener); + } + + public TransportFuture submitRequest(DiscoveryNode node, String action, TransportRequest request, + TransportResponseHandler handler) throws TransportException { + return submitRequest(node, action, request, TransportRequestOptions.EMPTY, handler); + } + + public TransportFuture submitRequest(DiscoveryNode node, String action, TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler) throws TransportException { + PlainTransportFuture futureHandler = new PlainTransportFuture<>(handler); + try { + Transport.Connection connection = getConnection(node); + sendRequest(connection, action, request, options, futureHandler); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + futureHandler.handleException(ex); + } + return futureHandler; + } + + public void sendRequest(final DiscoveryNode node, final String action, + final TransportRequest request, + final TransportResponseHandler handler) { + try { + Transport.Connection connection = getConnection(node); + sendRequest(connection, action, request, TransportRequestOptions.EMPTY, handler); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + handler.handleException(ex); + } + } + + public final void sendRequest(final DiscoveryNode node, final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + try { + Transport.Connection connection = getConnection(node); + sendRequest(connection, action, request, options, handler); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + handler.handleException(ex); + } + } + + public final void sendRequest(final Transport.Connection connection, final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + + asyncSender.sendRequest(connection, action, request, options, handler); + } + + /** + * Returns either a real transport connection or a local node connection if we are using the local node optimization. + * @throws NodeNotConnectedException if the given node is not connected + */ + public Transport.Connection getConnection(DiscoveryNode node) { + if (isLocalNode(node)) { + return localNodeConnection; + } else { + return transport.getConnection(node); + } + } + + public final void sendChildRequest(final DiscoveryNode node, final String action, + final TransportRequest request, final Task parentTask, + final TransportRequestOptions options, + final TransportResponseHandler handler) { + try { + Transport.Connection connection = getConnection(node); + sendChildRequest(connection, action, request, parentTask, options, handler); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + handler.handleException(ex); + } + } + + public void sendChildRequest(final Transport.Connection connection, final String action, + final TransportRequest request, final Task parentTask, + final TransportResponseHandler handler) { + sendChildRequest(connection, action, request, parentTask, TransportRequestOptions.EMPTY, handler); + } + + public void sendChildRequest(final Transport.Connection connection, final String action, + final TransportRequest request, final Task parentTask, + final TransportRequestOptions options, + final TransportResponseHandler handler) { + request.setParentTask(localNode.getId(), parentTask.getId()); + try { + sendRequest(connection, action, request, options, handler); + } catch (TaskCancelledException ex) { + // The parent task is already cancelled - just fail the request + handler.handleException(new TransportException(ex)); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + handler.handleException(ex); + } + + } + + private void sendRequestInternal(final Transport.Connection connection, final String action, + final TransportRequest request, + final TransportRequestOptions options, + TransportResponseHandler handler) { + if (connection == null) { + throw new IllegalStateException("can't send request to a null connection"); + } + DiscoveryNode node = connection.getNode(); + final long requestId = transport.newRequestId(); + final TimeoutHandler timeoutHandler; + try { + + if (options.timeout() == null) { + timeoutHandler = null; + } else { + timeoutHandler = new TimeoutHandler(requestId); + } + Supplier storedContextSupplier = threadPool.getThreadContext().newRestorableContext(true); + TransportResponseHandler responseHandler = new ContextRestoreResponseHandler<>(storedContextSupplier, handler); + clientHandlers.put(requestId, new RequestHolder<>(responseHandler, connection, action, timeoutHandler)); + if (lifecycle.stoppedOrClosed()) { + // if we are not started the exception handling will remove the RequestHolder again and calls the handler to notify + // the caller. It will only notify if the toStop code hasn't done the work yet. + throw new TransportException("TransportService is closed stopped can't send request"); + } + if (timeoutHandler != null) { + assert options.timeout() != null; + timeoutHandler.future = threadPool.schedule(options.timeout(), ThreadPool.Names.GENERIC, timeoutHandler); + } + connection.sendRequest(requestId, action, request, options); // local node optimization happens upstream + } catch (final Exception e) { + // usually happen either because we failed to connect to the node + // or because we failed serializing the message + final RequestHolder holderToNotify = clientHandlers.remove(requestId); + // If holderToNotify == null then handler has already been taken care of. + if (holderToNotify != null) { + holderToNotify.cancelTimeout(); + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + final SendRequestTransportException sendRequestException = new SendRequestTransportException(node, action, e); + threadPool.executor(ThreadPool.Names.GENERIC).execute(new AbstractRunnable() { + @Override + public void onRejection(Exception e) { + // if we get rejected during node shutdown we don't wanna bubble it up + logger.debug( + (Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on rejection, action: {}", + holderToNotify.action()), + e); + } + @Override + public void onFailure(Exception e) { + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "failed to notify response handler on exception, action: {}", + holderToNotify.action()), + e); + } + @Override + protected void doRun() throws Exception { + holderToNotify.handler().handleException(sendRequestException); + } + }); + } else { + logger.debug("Exception while sending request, handler likely already notified due to timeout", e); + } + } + } + + private void sendLocalRequest(long requestId, final String action, final TransportRequest request, TransportRequestOptions options) { + final DirectResponseChannel channel = new DirectResponseChannel(logger, localNode, action, requestId, this, threadPool); + try { + onRequestSent(localNode, requestId, action, request, options); + onRequestReceived(requestId, action); + final RequestHandlerRegistry reg = getRequestHandler(action); + if (reg == null) { + throw new ActionNotFoundTransportException("Action [" + action + "] not found"); + } + final String executor = reg.getExecutor(); + if (ThreadPool.Names.SAME.equals(executor)) { + //noinspection unchecked + reg.processMessageReceived(request, channel); + } else { + threadPool.executor(executor).execute(new AbstractRunnable() { + @Override + protected void doRun() throws Exception { + //noinspection unchecked + reg.processMessageReceived(request, channel); + } + + @Override + public boolean isForceExecution() { + return reg.isForceExecution(); + } + + @Override + public void onFailure(Exception e) { + try { + channel.sendResponse(e); + } catch (Exception inner) { + inner.addSuppressed(e); + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "failed to notify channel of error message for action [{}]", action), inner); + } + } + }); + } + + } catch (Exception e) { + try { + channel.sendResponse(e); + } catch (Exception inner) { + inner.addSuppressed(e); + logger.warn( + (Supplier) () -> new ParameterizedMessage( + "failed to notify channel of error message for action [{}]", action), inner); + } + } + } + + private boolean shouldTraceAction(String action) { + if (tracerLogInclude.length > 0) { + if (Regex.simpleMatch(tracerLogInclude, action) == false) { + return false; + } + } + if (tracerLogExclude.length > 0) { + return !Regex.simpleMatch(tracerLogExclude, action); + } + return true; + } + + public TransportAddress[] addressesFromString(String address, int perAddressLimit) throws UnknownHostException { + return transport.addressesFromString(address, perAddressLimit); + } + + /** + * Registers a new request handler + * + * @param action The action the request handler is associated with + * @param requestFactory a callable to be used construct new instances for streaming + * @param executor The executor the request handling will be executed on + * @param handler The handler itself that implements the request handling + */ + public void registerRequestHandler(String action, Supplier requestFactory, + String executor, TransportRequestHandler handler) { + handler = interceptor.interceptHandler(action, executor, false, handler); + RequestHandlerRegistry reg = new RequestHandlerRegistry<>( + action, Streamable.newWriteableReader(requestFactory), taskManager, handler, executor, false, true); + registerRequestHandler(reg); + } + + /** + * Registers a new request handler + * + * @param action The action the request handler is associated with + * @param requestReader a callable to be used construct new instances for streaming + * @param executor The executor the request handling will be executed on + * @param handler The handler itself that implements the request handling + */ + public void registerRequestHandler(String action, String executor, + Writeable.Reader requestReader, + TransportRequestHandler handler) { + handler = interceptor.interceptHandler(action, executor, false, handler); + RequestHandlerRegistry reg = new RequestHandlerRegistry<>( + action, requestReader, taskManager, handler, executor, false, true); + registerRequestHandler(reg); + } + + /** + * Registers a new request handler + * + * @param action The action the request handler is associated with + * @param request The request class that will be used to construct new instances for streaming + * @param executor The executor the request handling will be executed on + * @param forceExecution Force execution on the executor queue and never reject it + * @param canTripCircuitBreaker Check the request size and raise an exception in case the limit is breached. + * @param handler The handler itself that implements the request handling + */ + public void registerRequestHandler(String action, Supplier request, + String executor, boolean forceExecution, + boolean canTripCircuitBreaker, + TransportRequestHandler handler) { + handler = interceptor.interceptHandler(action, executor, forceExecution, handler); + RequestHandlerRegistry reg = new RequestHandlerRegistry<>( + action, Streamable.newWriteableReader(request), taskManager, handler, executor, forceExecution, canTripCircuitBreaker); + registerRequestHandler(reg); + } + + /** + * Registers a new request handler + * + * @param action The action the request handler is associated with + * @param requestReader The request class that will be used to construct new instances for streaming + * @param executor The executor the request handling will be executed on + * @param forceExecution Force execution on the executor queue and never reject it + * @param canTripCircuitBreaker Check the request size and raise an exception in case the limit is breached. + * @param handler The handler itself that implements the request handling + */ + public void registerRequestHandler(String action, + String executor, boolean forceExecution, + boolean canTripCircuitBreaker, + Writeable.Reader requestReader, + TransportRequestHandler handler) { + handler = interceptor.interceptHandler(action, executor, forceExecution, handler); + RequestHandlerRegistry reg = new RequestHandlerRegistry<>( + action, requestReader, taskManager, handler, executor, forceExecution, canTripCircuitBreaker); + registerRequestHandler(reg); + } + + private void registerRequestHandler(RequestHandlerRegistry reg) { + synchronized (requestHandlerMutex) { + if (requestHandlers.containsKey(reg.getAction())) { + throw new IllegalArgumentException("transport handlers for action " + reg.getAction() + " is already registered"); + } + requestHandlers = MapBuilder.newMapBuilder(requestHandlers).put(reg.getAction(), reg).immutableMap(); + } + } + + /** called by the {@link Transport} implementation once a request has been sent */ + void onRequestSent(DiscoveryNode node, long requestId, String action, TransportRequest request, + TransportRequestOptions options) { + if (traceEnabled() && shouldTraceAction(action)) { + traceRequestSent(node, requestId, action, options); + } + } + + protected boolean traceEnabled() { + return tracerLog.isTraceEnabled(); + } + + /** called by the {@link Transport} implementation once a response was sent to calling node */ + void onResponseSent(long requestId, String action, TransportResponse response, TransportResponseOptions options) { + if (traceEnabled() && shouldTraceAction(action)) { + traceResponseSent(requestId, action); + } + } + + /** called by the {@link Transport} implementation after an exception was sent as a response to an incoming request */ + void onResponseSent(long requestId, String action, Exception e) { + if (traceEnabled() && shouldTraceAction(action)) { + traceResponseSent(requestId, action, e); + } + } + + protected void traceResponseSent(long requestId, String action, Exception e) { + tracerLog.trace( + (org.apache.logging.log4j.util.Supplier) + () -> new ParameterizedMessage("[{}][{}] sent error response", requestId, action), e); + } + + /** + * called by the {@link Transport} implementation when an incoming request arrives but before + * any parsing of it has happened (with the exception of the requestId and action) + */ + void onRequestReceived(long requestId, String action) { + try { + blockIncomingRequestsLatch.await(); + } catch (InterruptedException e) { + logger.trace("interrupted while waiting for incoming requests block to be removed"); + } + if (traceEnabled() && shouldTraceAction(action)) { + traceReceivedRequest(requestId, action); + } + } + + public RequestHandlerRegistry getRequestHandler(String action) { + return requestHandlers.get(action); + } + + /** + * called by the {@link Transport} implementation when a response or an exception has been received for a previously + * sent request (before any processing or deserialization was done). Returns the appropriate response handler or null if not + * found. + */ + public TransportResponseHandler onResponseReceived(final long requestId) { + RequestHolder holder = clientHandlers.remove(requestId); + + if (holder == null) { + checkForTimeout(requestId); + return null; + } + holder.cancelTimeout(); + if (traceEnabled() && shouldTraceAction(holder.action())) { + traceReceivedResponse(requestId, holder.connection().getNode(), holder.action()); + } + return holder.handler(); + } + + private void checkForTimeout(long requestId) { + // lets see if its in the timeout holder, but sync on mutex to make sure any ongoing timeout handling has finished + final DiscoveryNode sourceNode; + final String action; + assert clientHandlers.get(requestId) == null; + TimeoutInfoHolder timeoutInfoHolder = timeoutInfoHandlers.remove(requestId); + if (timeoutInfoHolder != null) { + long time = System.currentTimeMillis(); + logger.warn("Received response for a request that has timed out, sent [{}ms] ago, timed out [{}ms] ago, " + + "action [{}], node [{}], id [{}]", time - timeoutInfoHolder.sentTime(), time - timeoutInfoHolder.timeoutTime(), + timeoutInfoHolder.action(), timeoutInfoHolder.node(), requestId); + action = timeoutInfoHolder.action(); + sourceNode = timeoutInfoHolder.node(); + } else { + logger.warn("Transport response handler not found of id [{}]", requestId); + action = null; + sourceNode = null; + } + // call tracer out of lock + if (traceEnabled() == false) { + return; + } + if (action == null) { + assert sourceNode == null; + traceUnresolvedResponse(requestId); + } else if (shouldTraceAction(action)) { + traceReceivedResponse(requestId, sourceNode, action); + } + } + + void onNodeConnected(final DiscoveryNode node) { + // capture listeners before spawning the background callback so the following pattern won't trigger a call + // connectToNode(); connection is completed successfully + // addConnectionListener(); this listener shouldn't be called + final Stream listenersToNotify = TransportService.this.connectionListeners.stream(); + getExecutorService().execute(() -> listenersToNotify.forEach(listener -> listener.onNodeConnected(node))); + } + + void onConnectionOpened(Transport.Connection connection) { + // capture listeners before spawning the background callback so the following pattern won't trigger a call + // connectToNode(); connection is completed successfully + // addConnectionListener(); this listener shouldn't be called + final Stream listenersToNotify = TransportService.this.connectionListeners.stream(); + getExecutorService().execute(() -> listenersToNotify.forEach(listener -> listener.onConnectionOpened(connection))); + } + + public void onNodeDisconnected(final DiscoveryNode node) { + try { + getExecutorService().execute( () -> { + for (final TransportConnectionListener connectionListener : connectionListeners) { + connectionListener.onNodeDisconnected(node); + } + }); + } catch (EsRejectedExecutionException ex) { + logger.debug("Rejected execution on NodeDisconnected", ex); + } + } + + void onConnectionClosed(Transport.Connection connection) { + try { + for (Map.Entry entry : clientHandlers.entrySet()) { + RequestHolder holder = entry.getValue(); + if (holder.connection().getCacheKey().equals(connection.getCacheKey())) { + final RequestHolder holderToNotify = clientHandlers.remove(entry.getKey()); + if (holderToNotify != null) { + // callback that an exception happened, but on a different thread since we don't + // want handlers to worry about stack overflows + getExecutorService().execute(() -> holderToNotify.handler().handleException(new NodeDisconnectedException( + connection.getNode(), holderToNotify.action()))); + } + } + } + } catch (EsRejectedExecutionException ex) { + logger.debug("Rejected execution on onConnectionClosed", ex); + } + } + + protected void traceReceivedRequest(long requestId, String action) { + tracerLog.trace("[{}][{}] received request", requestId, action); + } + + protected void traceResponseSent(long requestId, String action) { + tracerLog.trace("[{}][{}] sent response", requestId, action); + } + + protected void traceReceivedResponse(long requestId, DiscoveryNode sourceNode, String action) { + tracerLog.trace("[{}][{}] received response from [{}]", requestId, action, sourceNode); + } + + protected void traceUnresolvedResponse(long requestId) { + tracerLog.trace("[{}] received response but can't resolve it to a request", requestId); + } + + protected void traceRequestSent(DiscoveryNode node, long requestId, String action, TransportRequestOptions options) { + tracerLog.trace("[{}][{}] sent to [{}] (timeout: [{}])", requestId, action, node, options.timeout()); + } + + class TimeoutHandler implements Runnable { + + private final long requestId; + + private final long sentTime = System.currentTimeMillis(); + + volatile ScheduledFuture future; + + TimeoutHandler(long requestId) { + this.requestId = requestId; + } + + @Override + public void run() { + // we get first to make sure we only add the TimeoutInfoHandler if needed. + final RequestHolder holder = clientHandlers.get(requestId); + if (holder != null) { + // add it to the timeout information holder, in case we are going to get a response later + long timeoutTime = System.currentTimeMillis(); + timeoutInfoHandlers.put(requestId, new TimeoutInfoHolder(holder.connection().getNode(), holder.action(), sentTime, + timeoutTime)); + // now that we have the information visible via timeoutInfoHandlers, we try to remove the request id + final RequestHolder removedHolder = clientHandlers.remove(requestId); + if (removedHolder != null) { + assert removedHolder == holder : "two different holder instances for request [" + requestId + "]"; + removedHolder.handler().handleException( + new ReceiveTimeoutTransportException(holder.connection().getNode(), holder.action(), + "request_id [" + requestId + "] timed out after [" + (timeoutTime - sentTime) + "ms]")); + } else { + // response was processed, remove timeout info. + timeoutInfoHandlers.remove(requestId); + } + } + } + + /** + * cancels timeout handling. this is a best effort only to avoid running it. remove the requestId from {@link #clientHandlers} + * to make sure this doesn't run. + */ + public void cancel() { + assert clientHandlers.get(requestId) == null : + "cancel must be called after the requestId [" + requestId + "] has been removed from clientHandlers"; + FutureUtils.cancel(future); + } + } + + static class TimeoutInfoHolder { + + private final DiscoveryNode node; + private final String action; + private final long sentTime; + private final long timeoutTime; + + TimeoutInfoHolder(DiscoveryNode node, String action, long sentTime, long timeoutTime) { + this.node = node; + this.action = action; + this.sentTime = sentTime; + this.timeoutTime = timeoutTime; + } + + public DiscoveryNode node() { + return node; + } + + public String action() { + return action; + } + + public long sentTime() { + return sentTime; + } + + public long timeoutTime() { + return timeoutTime; + } + } + + static class RequestHolder { + + private final TransportResponseHandler handler; + + private final Transport.Connection connection; + + private final String action; + + private final TimeoutHandler timeoutHandler; + + RequestHolder(TransportResponseHandler handler, Transport.Connection connection, String action, TimeoutHandler timeoutHandler) { + this.handler = handler; + this.connection = connection; + this.action = action; + this.timeoutHandler = timeoutHandler; + } + + public TransportResponseHandler handler() { + return handler; + } + + public Transport.Connection connection() { + return this.connection; + } + + public String action() { + return this.action; + } + + public void cancelTimeout() { + if (timeoutHandler != null) { + timeoutHandler.cancel(); + } + } + } + + /** + * This handler wrapper ensures that the response thread executes with the correct thread context. Before any of the handle methods + * are invoked we restore the context. + */ + public static final class ContextRestoreResponseHandler implements TransportResponseHandler { + + private final TransportResponseHandler delegate; + private final Supplier contextSupplier; + + public ContextRestoreResponseHandler(Supplier contextSupplier, TransportResponseHandler delegate) { + this.delegate = delegate; + this.contextSupplier = contextSupplier; + } + + @Override + public T read(StreamInput in) throws IOException { + return delegate.read(in); + } + + @Override + public void handleResponse(T response) { + try (ThreadContext.StoredContext ignore = contextSupplier.get()) { + delegate.handleResponse(response); + } + } + + @Override + public void handleException(TransportException exp) { + try (ThreadContext.StoredContext ignore = contextSupplier.get()) { + delegate.handleException(exp); + } + } + + @Override + public String executor() { + return delegate.executor(); + } + + @Override + public String toString() { + return getClass().getName() + "/" + delegate.toString(); + } + + } + + static class DirectResponseChannel implements TransportChannel { + final Logger logger; + final DiscoveryNode localNode; + private final String action; + private final long requestId; + final TransportService service; + final ThreadPool threadPool; + + DirectResponseChannel(Logger logger, DiscoveryNode localNode, String action, long requestId, TransportService service, + ThreadPool threadPool) { + this.logger = logger; + this.localNode = localNode; + this.action = action; + this.requestId = requestId; + this.service = service; + this.threadPool = threadPool; + } + + @Override + public String getProfileName() { + return DIRECT_RESPONSE_PROFILE; + } + + @Override + public void sendResponse(TransportResponse response) throws IOException { + sendResponse(response, TransportResponseOptions.EMPTY); + } + + @Override + public void sendResponse(final TransportResponse response, TransportResponseOptions options) throws IOException { + service.onResponseSent(requestId, action, response, options); + final TransportResponseHandler handler = service.onResponseReceived(requestId); + // ignore if its null, the service logs it + if (handler != null) { + final String executor = handler.executor(); + if (ThreadPool.Names.SAME.equals(executor)) { + processResponse(handler, response); + } else { + threadPool.executor(executor).execute(() -> processResponse(handler, response)); + } + } + } + + @SuppressWarnings("unchecked") + protected void processResponse(TransportResponseHandler handler, TransportResponse response) { + try { + handler.handleResponse(response); + } catch (Exception e) { + processException(handler, wrapInRemote(new ResponseHandlerFailureTransportException(e))); + } + } + + @Override + public void sendResponse(Exception exception) throws IOException { + service.onResponseSent(requestId, action, exception); + final TransportResponseHandler handler = service.onResponseReceived(requestId); + // ignore if its null, the service logs it + if (handler != null) { + final RemoteTransportException rtx = wrapInRemote(exception); + final String executor = handler.executor(); + if (ThreadPool.Names.SAME.equals(executor)) { + processException(handler, rtx); + } else { + threadPool.executor(handler.executor()).execute(() -> processException(handler, rtx)); + } + } + } + + protected RemoteTransportException wrapInRemote(Exception e) { + if (e instanceof RemoteTransportException) { + return (RemoteTransportException) e; + } + return new RemoteTransportException(localNode.getName(), localNode.getAddress(), action, e); + } + + protected void processException(final TransportResponseHandler handler, final RemoteTransportException rtx) { + try { + handler.handleException(rtx); + } catch (Exception e) { + logger.error( + (Supplier) () -> new ParameterizedMessage( + "failed to handle exception for action [{}], handler [{}]", action, handler), e); + } + } + + @Override + public String getChannelType() { + return "direct"; + } + + @Override + public Version getVersion() { + return localNode.getVersion(); + } + } + + /** + * Returns the internal thread pool + */ + public ThreadPool getThreadPool() { + return threadPool; + } + + private boolean isLocalNode(DiscoveryNode discoveryNode) { + return Objects.requireNonNull(discoveryNode, "discovery node must not be null").equals(localNode); + } +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportStatus.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportStatus.java new file mode 100644 index 0000000..ed69ad5 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/TransportStatus.java @@ -0,0 +1,52 @@ +package org.xbib.elasticsearch.client.transport; + +public final class TransportStatus { + + private static final byte STATUS_REQRES = 1 << 0; + private static final byte STATUS_ERROR = 1 << 1; + private static final byte STATUS_COMPRESS = 1 << 2; + private static final byte STATUS_HANDSHAKE = 1 << 3; + + public static boolean isRequest(byte value) { + return (value & STATUS_REQRES) == 0; + } + + public static byte setRequest(byte value) { + value &= ~STATUS_REQRES; + return value; + } + + public static byte setResponse(byte value) { + value |= STATUS_REQRES; + return value; + } + + public static boolean isError(byte value) { + return (value & STATUS_ERROR) != 0; + } + + public static byte setError(byte value) { + value |= STATUS_ERROR; + return value; + } + + public static boolean isCompress(byte value) { + return (value & STATUS_COMPRESS) != 0; + } + + public static byte setCompress(byte value) { + value |= STATUS_COMPRESS; + return value; + } + + static boolean isHandshake(byte value) { // pkg private since it's only used internally + return (value & STATUS_HANDSHAKE) != 0; + } + + static byte setHandshake(byte value) { // pkg private since it's only used internally + value |= STATUS_HANDSHAKE; + return value; + } + + +} diff --git a/transport/src/main/java/org/xbib/elasticsearch/client/transport/package-info.java b/transport/src/main/java/org/xbib/elasticsearch/client/transport/package-info.java new file mode 100644 index 0000000..50220b5 --- /dev/null +++ b/transport/src/main/java/org/xbib/elasticsearch/client/transport/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Elasticsearch transport client. + */ +package org.xbib.elasticsearch.client.transport; diff --git a/transport/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods b/transport/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods new file mode 100644 index 0000000..c94ea28 --- /dev/null +++ b/transport/src/main/resources/META-INF/services/org.xbib.elasticsearch.client.ClientMethods @@ -0,0 +1 @@ +org.xbib.elasticsearch.client.transport.TransportBulkClient \ No newline at end of file diff --git a/transport/src/main/resources/extra-security.policy b/transport/src/main/resources/extra-security.policy new file mode 100644 index 0000000..24db998 --- /dev/null +++ b/transport/src/main/resources/extra-security.policy @@ -0,0 +1,15 @@ + +grant codeBase "${codebase.netty-common}" { + // for reading the system-wide configuration for the backlog of established sockets + permission java.io.FilePermission "/proc/sys/net/core/somaxconn", "read"; + // netty makes and accepts socket connections + permission java.net.SocketPermission "*", "accept,connect,resolve"; + // 4.1.24 io.netty.util.concurrent.GlobalEventExecutor$2.run(GlobalEventExecutor.java:228) + permission java.lang.RuntimePermission "setContextClassLoader"; +}; + +grant codeBase "${codebase.netty-transport}" { + // Netty NioEventLoop wants to change this, because of https://bugs.openjdk.java.net/browse/JDK-6427854 + // the bug says it only happened rarely, and that its fixed, but apparently it still happens rarely! + permission java.util.PropertyPermission "sun.nio.ch.bugLevel", "write"; +}; diff --git a/transport/src/test/java/org/xbib/elasticsearch/client/transport/TestRunnerThreadsFilter.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TestRunnerThreadsFilter.java new file mode 100644 index 0000000..0ad52e3 --- /dev/null +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TestRunnerThreadsFilter.java @@ -0,0 +1,11 @@ +package org.xbib.elasticsearch.client.transport; + +import com.carrotsearch.randomizedtesting.ThreadFilter; + +public class TestRunnerThreadsFilter implements ThreadFilter { + + @Override + public boolean reject(Thread thread) { + return thread.getName().startsWith("ObjectCleanerThread"); + } +} diff --git a/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientDuplicateIDTests.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientDuplicateIDTests.java new file mode 100644 index 0000000..52bb8df --- /dev/null +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientDuplicateIDTests.java @@ -0,0 +1,107 @@ +package org.xbib.elasticsearch.client.transport; + +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +import java.util.Collection; +import java.util.Collections; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class TransportBulkClientDuplicateIDTests extends ESSingleNodeTestCase { + + private static final Logger logger = LogManager.getLogger(TransportBulkClientDuplicateIDTests.class.getName()); + + private static final long MAX_ACTIONS = 100L; + + private static final long NUM_ACTIONS = 12345L; + + private TransportAddress address; + + @Before + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + address = nodeInfo.getTransport().getAddress().publishAddress(); + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), "test-cluster") + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .build(); + } + + private Settings transportClientSettings() { + return Settings.builder() + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), "test-cluster") + .put("host", address.address().getHostString() + ":" + address.getPort()) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) // limit the number of threads created + .build(); + } + + public void testDuplicateDocIDs() throws Exception { + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) + .put(ClientBuilder.MAX_CONCURRENT_REQUESTS, 2) // avoid EsRejectedExecutionException + .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(TransportBulkClient.class); + try { + client.newIndex("test"); + for (int i = 0; i < NUM_ACTIONS; i++) { + client.index("test", "test", randomAlphaOfLength(1), false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + client.refreshIndex("test"); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client.client(), SearchAction.INSTANCE) + .setIndices("test") + .setTypes("test") + .setQuery(matchAllQuery()); + long hits = searchRequestBuilder.execute().actionGet().getHits().getTotalHits(); + logger.info("hits = {}", hits); + assertTrue(hits < NUM_ACTIONS); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + logger.info("numactions = {}, submitted = {}, succeeded= {}, failed = {}", NUM_ACTIONS, + client.getMetric().getSubmitted().getCount(), + client.getMetric().getSucceeded().getCount(), + client.getMetric().getFailed().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSubmitted().getCount()); + assertEquals(NUM_ACTIONS, client.getMetric().getSucceeded().getCount()); + } + } +} diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientReplicaTests.java similarity index 63% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java rename to transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientReplicaTests.java index ffad2c7..7168304 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportReplicaTest.java +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientReplicaTests.java @@ -1,11 +1,11 @@ -package org.xbib.elasticsearch.extras.client.transport; +package org.xbib.elasticsearch.client.transport; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.indices.stats.CommonStats; import org.elasticsearch.action.admin.indices.stats.IndexShardStats; import org.elasticsearch.action.admin.indices.stats.IndexStats; @@ -15,56 +15,73 @@ import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.index.shard.IndexingStats; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.elasticsearch.test.ESIntegTestCase; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; import java.util.Map; -/** - * - */ -public class BulkTransportReplicaTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class TransportBulkClientReplicaTests extends ESIntegTestCase { - private static final Logger logger = LogManager.getLogger(BulkTransportClientTest.class.getName()); + private static final Logger logger = LogManager.getLogger(TransportBulkClientTests.class.getName()); + + private String clusterName; + + private TransportAddress address; + + @Before + public void fetchTransportAddress() { + clusterName = client().admin().cluster().prepareClusterStats().get().getClusterName().value(); + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + address = nodeInfo.getTransport().getAddress().publishAddress(); + } + + private Settings ourTransportClientSettings() { + return Settings.builder() + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("host", address.address().getHostString() + ":" + address.getPort()) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) // limit the number of threads created + .build(); + } - @Test public void testReplicaLevel() throws Exception { - // we need nodes for replica levels - startNode("2"); - startNode("3"); - startNode("4"); + //ensureStableCluster(4); Settings settingsTest1 = Settings.builder() - .put("index.number_of_shards", 2) - .put("index.number_of_replicas", 3) + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 2) .build(); Settings settingsTest2 = Settings.builder() - .put("index.number_of_shards", 2) + .put("index.number_of_shards", 1) .put("index.number_of_replicas", 1) .build(); - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(ourTransportClientSettings()) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); + .getClient(TransportBulkClient.class); try { client.newIndex("test1", settingsTest1, null) .newIndex("test2", settingsTest2, null); client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); for (int i = 0; i < 1234; i++) { - client.index("test1", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test1", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } for (int i = 0; i < 1234; i++) { - client.index("test2", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test2", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(60)); @@ -109,5 +126,4 @@ public class BulkTransportReplicaTest extends NodeTestBase { assertFalse(client.hasThrowable()); } } - } diff --git a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientTests.java similarity index 65% rename from src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java rename to transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientTests.java index 92ed78c..5b9375e 100644 --- a/src/integration-test/java/org/xbib/elasticsearch/extras/client/transport/BulkTransportClientTest.java +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientTests.java @@ -1,76 +1,100 @@ -package org.xbib.elasticsearch.extras.client.transport; - -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +package org.xbib.elasticsearch.client.transport; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.Netty4Plugin; import org.junit.Before; -import org.junit.Test; -import org.xbib.elasticsearch.NodeTestBase; -import org.xbib.elasticsearch.extras.client.ClientBuilder; -import org.xbib.elasticsearch.extras.client.SimpleBulkControl; -import org.xbib.elasticsearch.extras.client.SimpleBulkMetric; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; import java.io.IOException; +import java.util.Collection; +import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -/** - * - */ -public class BulkTransportClientTest extends NodeTestBase { +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +public class TransportBulkClientTests extends ESSingleNodeTestCase { - private static final Logger logger = LogManager.getLogger(BulkTransportClientTest.class.getName()); + private static final Logger logger = LogManager.getLogger(TransportBulkClientTests.class.getName()); - private static final Long MAX_ACTIONS = 1000L; + private static final Long MAX_ACTIONS = 10L; private static final Long NUM_ACTIONS = 1234L; + private TransportAddress address; + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(Netty4Plugin.class); + } + + @Override + public Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), "test-cluster") + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .build(); + } + + private Settings transportClientSettings() { + return Settings.builder() + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), "test-cluster") + .put("host", address.address().getHostString() + ":" + address.getPort()) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) // limit the number of threads created + .build(); + } + @Before - public void startNodes() { - try { - super.startNodes(); - startNode("2"); - } catch (Throwable t) { - logger.error("startNodes failed", t); - } + public void fetchTransportAddress() { + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + address = nodeInfo.getTransport().getAddress().publishAddress(); } - @Test - public void testBulkTransportClientNewIndex() throws IOException { + public void testBulkTransportClientNewIndex() throws Exception { logger.info("firing up BulkTransportClient"); - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); - logger.info("creating index"); - client.newIndex("test"); - if (client.hasThrowable()) { - logger.error("error", client.getThrowable()); - } - assertFalse(client.hasThrowable()); + .getClient(TransportBulkClient.class); try { - logger.info("deleting/creating index sequence start"); + logger.info("creating index"); + client.newIndex("test"); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + logger.info("deleting/creating index: start"); client.deleteIndex("test") .newIndex("test") .deleteIndex("test"); - logger.info("deleting/creating index sequence end"); + logger.info("deleting/creating index: end"); } catch (NoNodeAvailableException e) { logger.error("no node available"); } finally { @@ -82,15 +106,14 @@ public class BulkTransportClientTest extends NodeTestBase { } } - @Test public void testBulkTransportClientMapping() throws Exception { - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(5)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); - XContentBuilder builder = jsonBuilder() + .getClient(TransportBulkClient.class); + XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .startObject("test") .startObject("properties") @@ -113,21 +136,20 @@ public class BulkTransportClientTest extends NodeTestBase { client.shutdown(); } - @Test public void testBulkTransportClientSingleDoc() throws IOException { logger.info("firing up BulkTransportClient"); - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); + .getClient(TransportBulkClient.class); try { logger.info("creating index"); client.newIndex("test"); logger.info("indexing one doc"); - client.index("test", "test", "1", "{ \"name\" : \"Hello World\"}"); // single doc ingest + client.index("test", "test", "1", false, "{ \"name\" : \"Hello World\"}"); // single doc ingest logger.info("flush"); client.flushIngest(); logger.info("wait for responses"); @@ -149,20 +171,19 @@ public class BulkTransportClientTest extends NodeTestBase { } } - @Test public void testBulkTransportClientRandomDocs() throws Exception { long numactions = NUM_ACTIONS; - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, MAX_ACTIONS) .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); + .getClient(TransportBulkClient.class); try { client.newIndex("test"); for (int i = 0; i < NUM_ACTIONS; i++) { - client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } client.flushIngest(); client.waitForResponses(TimeValue.timeValueSeconds(30)); @@ -179,19 +200,18 @@ public class BulkTransportClientTest extends NodeTestBase { } } - @Test public void testBulkTransportClientThreadedRandomDocs() throws Exception { int maxthreads = Runtime.getRuntime().availableProcessors(); long maxactions = MAX_ACTIONS; final long maxloop = NUM_ACTIONS; logger.info("TransportClient max={} maxactions={} maxloop={}", maxthreads, maxactions, maxloop); - final BulkTransportClient client = ClientBuilder.builder() - .put(getClientSettings()) + final TransportBulkClient client = ClientBuilder.builder() + .put(transportClientSettings()) .put(ClientBuilder.MAX_ACTIONS_PER_REQUEST, maxactions) - .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) // = disable autoflush for this test + .put(ClientBuilder.FLUSH_INTERVAL, TimeValue.timeValueSeconds(60)) // = effectively disables autoflush for this test .setMetric(new SimpleBulkMetric()) .setControl(new SimpleBulkControl()) - .toBulkTransportClient(); + .getClient(TransportBulkClient.class); try { client.newIndex("test").startBulk("test", 30 * 1000, 1000); ExecutorService executorService = Executors.newFixedThreadPool(maxthreads); @@ -199,7 +219,7 @@ public class BulkTransportClientTest extends NodeTestBase { for (int i = 0; i < maxthreads; i++) { executorService.execute(() -> { for (int i1 = 0; i1 < maxloop; i1++) { - client.index("test", "test", null, "{ \"name\" : \"" + randomString(32) + "\"}"); + client.index("test", "test", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); } latch.countDown(); }); diff --git a/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientUpdateReplicaLevelTests.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientUpdateReplicaLevelTests.java new file mode 100644 index 0000000..d47ba30 --- /dev/null +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/TransportBulkClientUpdateReplicaLevelTests.java @@ -0,0 +1,82 @@ +package org.xbib.elasticsearch.client.transport; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.test.ESIntegTestCase; +import org.junit.Before; +import org.xbib.elasticsearch.client.ClientBuilder; +import org.xbib.elasticsearch.client.SimpleBulkControl; +import org.xbib.elasticsearch.client.SimpleBulkMetric; + +@ThreadLeakFilters(defaultFilters = true, filters = {TestRunnerThreadsFilter.class}) +@ESIntegTestCase.ClusterScope(scope=ESIntegTestCase.Scope.SUITE, numDataNodes=3) +public class TransportBulkClientUpdateReplicaLevelTests extends ESIntegTestCase { + + private static final Logger logger = LogManager.getLogger(TransportBulkClientUpdateReplicaLevelTests.class.getName()); + + private String clusterName; + + private TransportAddress address; + + @Before + public void fetchClusterInfo() { + clusterName = client().admin().cluster().prepareClusterStats().get().getClusterName().value(); + NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0); + address = nodeInfo.getTransport().getAddress().publishAddress(); + } + + private Settings ourTransportClientSettings() { + return Settings.builder() + .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), clusterName) + .put("host", address.address().getHostString() + ":" + address.getPort()) + .put(EsExecutors.PROCESSORS_SETTING.getKey(), 1) // limit the number of threads created + .build(); + } + + public void testUpdateReplicaLevel() throws Exception { + + //ensureStableCluster(3); + + int shardsAfterReplica; + + Settings settings = Settings.builder() + .put("index.number_of_shards", 2) + .put("index.number_of_replicas", 0) + .build(); + + final TransportBulkClient client = ClientBuilder.builder() + .put(ourTransportClientSettings()) + .setMetric(new SimpleBulkMetric()) + .setControl(new SimpleBulkControl()) + .getClient(TransportBulkClient.class); + + try { + client.newIndex("replicatest", settings, null); + client.waitForCluster("GREEN", TimeValue.timeValueSeconds(30)); + for (int i = 0; i < 12345; i++) { + client.index("replicatest", "replicatest", null, false, "{ \"name\" : \"" + randomAlphaOfLength(32) + "\"}"); + } + client.flushIngest(); + client.waitForResponses(TimeValue.timeValueSeconds(30)); + shardsAfterReplica = client.updateReplicaLevel("replicatest", 3); + assertEquals(shardsAfterReplica, 2 * (3 + 1)); + } catch (NoNodeAvailableException e) { + logger.warn("skipping, no node available"); + } finally { + client.shutdown(); + if (client.hasThrowable()) { + logger.error("error", client.getThrowable()); + } + assertFalse(client.hasThrowable()); + } + } + +} diff --git a/transport/src/test/java/org/xbib/elasticsearch/client/transport/package-info.java b/transport/src/test/java/org/xbib/elasticsearch/client/transport/package-info.java new file mode 100644 index 0000000..3b21564 --- /dev/null +++ b/transport/src/test/java/org/xbib/elasticsearch/client/transport/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing the transport client. + */ +package org.xbib.elasticsearch.client.transport; From 6611a55cbadfab0a9deaaff9c50b3b8b363b9b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Sun, 10 Feb 2019 20:39:07 +0100 Subject: [PATCH 11/11] update to 6.3.2.4, update to Gradle 5.1 --- api/build.gradle | 2 +- build.gradle | 75 +++++++++++------- common/build.gradle | 10 ++- gradle.properties | 34 ++++---- gradle/ext.gradle | 8 -- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 2 +- gradlew.bat | 2 +- http/build.gradle | 10 ++- .../node/info/HttpNodesInfoAction.java | 5 +- .../HttpClusterUpdateSettingsAction.java | 3 +- .../indices/create/HttpCreateIndexAction.java | 4 +- .../put/HttpUpdateSettingsAction.java | 3 +- .../elasticsearch/client/http/HttpAction.java | 2 + node/build.gradle | 10 ++- settings.gradle | 1 - transport/build.gradle | 12 +-- .../client/transport/ConnectionProfile.java | 2 +- .../transport/RemoteConnectionInfo.java | 4 +- 20 files changed, 110 insertions(+), 82 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 02e43a4..61be444 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -7,7 +7,7 @@ dependencies { exclude group: 'org.elasticsearch.plugin', module: 'percolator-client' exclude group: 'org.elasticsearch.plugin', module: 'lang-mustache-client' } - // we try to override the Elasticsearch netty by our netty version which might be more recent + // we try to override the Elasticsearch netty by our netty version which is more recent compile "io.netty:netty-buffer:${rootProject.property('netty.version')}" compile "io.netty:netty-codec-http:${rootProject.property('netty.version')}" compile "io.netty:netty-handler:${rootProject.property('netty.version')}" diff --git a/build.gradle b/build.gradle index 4a6e6c8..49dfe7d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,27 @@ +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +buildscript { + repositories { + jcenter() + maven { + url 'http://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.3.2.4" + } +} plugins { id "org.sonarqube" version "2.6.1" id "io.codearte.nexus-staging" version "0.11.0" - id "org.xbib.gradle.plugin.asciidoctor" version "1.6.0.0" + id "org.xbib.gradle.plugin.asciidoctor" version "1.6.0.1" } -printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + +printf "Date: %s\nHost: %s\nOS: %s %s %s\nJava: %s %s %s %s\nGradle: %s Groovy: %s Java: %s\n" + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", + ZonedDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME), InetAddress.getLocalHost(), System.getProperty("os.name"), System.getProperty("os.arch"), @@ -15,19 +30,25 @@ printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + System.getProperty("java.vm.version"), System.getProperty("java.vm.vendor"), System.getProperty("java.vm.name"), - GroovySystem.getVersion(), - gradle.gradleVersion + gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current() + apply plugin: "io.codearte.nexus-staging" +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +ext { + user = 'jprante' + name = 'elx' + description = 'Elasticsearch extensions' + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' +} subprojects { apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' - //apply plugin: 'findbugs' - //apply plugin: 'pmd' - //apply plugin: 'checkstyle' - apply plugin: 'org.xbib.gradle.plugin.asciidoctor' configurations { wagon @@ -36,36 +57,24 @@ subprojects { } dependencies { - //testCompile "junit:junit:${project.property('junit.version')}" - //testCompile "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}" wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" } - sourceCompatibility = JavaVersion.VERSION_1_9 - targetCompatibility = JavaVersion.VERSION_1_8 - [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' - tasks.withType(JavaCompile) { - options.compilerArgs << "-proc:none" << "-Xlint:all,-rawtypes,-unchecked,-try" << "--release" << "8" //<< "-profile" << "compact3" + compileJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } jar { baseName "${rootProject.name}-${project.name}" } - asciidoctor { - attributes toc: 'left', - doctype: 'book', - icons: 'font', - encoding: 'utf-8', - sectlink: true, - sectanchors: true, - linkattrs: true, - imagesdir: 'img', - 'source-highlighter': 'coderay' - } - javadoc { options.docletpath = configurations.asciidoclet.files.asType(List) options.doclet = 'org.xbib.asciidoclet.Asciidoclet' @@ -108,6 +117,18 @@ subprojects { } +/*asciidoctor { + attributes toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + 'source-highlighter': 'coderay' +}*/ + /* task aggregatedJavadoc(type: Javadoc) { group = 'aggregation' diff --git a/common/build.gradle b/common/build.gradle index 8638f17..7e0f3cb 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -6,7 +6,7 @@ buildscript { } } dependencies { - classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.2.2.0" + classpath "org.xbib.elasticsearch:gradle-plugin-elasticsearch-build:6.3.2.4" } } @@ -21,18 +21,20 @@ dependencies { compile project(':api') compile "org.xbib:metrics:${project.property('xbib-metrics.version')}" compileOnly "org.apache.logging.log4j:log4j-api:${project.property('log4j.version')}" - testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" - testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('xbib-elasticsearch-test.version')}" + testCompile "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('elasticsearch-devkit.version')}" + testRuntime "org.xbib.elasticsearch:elasticsearch-test-framework:${project.property('elasticsearch-devkit.version')}" } jar { baseName "${rootProject.name}-common" } +/* task testJar(type: Jar, dependsOn: testClasses) { baseName = "${project.archivesBaseName}-tests" from sourceSets.test.output } +*/ artifacts { main jar @@ -56,7 +58,7 @@ randomizedTest { esTest { // test with the jars, not the classes, for security manager - classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files + // classpath = files(configurations.testRuntime) + configurations.main.artifacts.files + configurations.tests.artifacts.files systemProperty 'tests.security.manager', 'true' } esTest.dependsOn jar, testJar diff --git a/gradle.properties b/gradle.properties index f75801d..24bd424 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,28 +1,30 @@ -org.gradle.daemon=false -org.gradle.warning.mode=all - group = org.xbib.elasticsearch -name = elasticsearch-client-netty -version = 6.2.2.0 +name = elasticsearch-client +version = 6.3.2.0 +profile = default +release = 0 -elasticsearch.version = 6.2.2 -netty.version = 4.1.24.Final -tcnative.version = 2.0.7.Final +elasticsearch.version = 6.3.2 +lucene.version = 7.3.1 + +netty.version = 4.1.29.Final +tcnative.version = 2.0.15.Final alpnagent.version = 2.0.7 -#xbib-netty-http-client.version = 4.1.16.1 -xbib-netty-http-client.version = 4.1.24.0 +xbib-netty-http-client.version = 4.1.29.0 xbib-metrics.version = 1.1.0 # elasticsearch build plugin -xbib-elasticsearch-test.version = 6.2.2.0 -lucene.version = 7.2.1 -spatial4j.version = 0.6 -jts.version = 1.13 -jna.version = 4.5.1 +elasticsearch-libs.version = 6.3.2.1 +elasticsearch-devkit.version = 6.3.2.4 +spatial4j.version = 0.7 +jts.version = 1.15.1 +jna.version = 4.5.2 +log4j.version = 2.11.1 +checkstyle.version = 8.13 # test -log4j.version = 2.9.1 junit.version = 4.12 wagon.version = 3.0.0 asciidoclet.version = 1.6.0.0 +org.gradle.warning.mode=all diff --git a/gradle/ext.gradle b/gradle/ext.gradle index 2318dcd..e69de29 100644 --- a/gradle/ext.gradle +++ b/gradle/ext.gradle @@ -1,8 +0,0 @@ -ext { - user = 'xbib' - name = 'elasticsearch-java-client' - description = 'Netty Java client for Elasticsearch' - scmUrl = 'https://github.com/' + user + '/' + name - scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c8b802289c3a438766657a5e98f20eff03..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch literal 55190 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 z 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 attributes = Collections.emptyMap(); Set roles = new HashSet<>(); Version version = Version.fromString((String) map2.get("version")); diff --git a/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java b/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java index 971e1c7..b66675c 100644 --- a/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java +++ b/http/src/main/java/org/elasticsearch/action/admin/cluster/settings/HttpClusterUpdateSettingsAction.java @@ -1,6 +1,7 @@ package org.elasticsearch.action.admin.cluster.settings; import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -32,7 +33,7 @@ public class HttpClusterUpdateSettingsAction extends HttpAction