From d31abab8278fd2c39de0c63dfaff28ee1360434f Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Mon, 18 Jan 2016 20:39:09 +0100 Subject: [PATCH 01/49] add note about `index.gateway.local.sync` to 2.0 migration/settings docs --- docs/reference/migration/migrate_2_0/settings.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/migration/migrate_2_0/settings.asciidoc b/docs/reference/migration/migrate_2_0/settings.asciidoc index 60f80b04e93..5e840ac3653 100644 --- a/docs/reference/migration/migrate_2_0/settings.asciidoc +++ b/docs/reference/migration/migrate_2_0/settings.asciidoc @@ -126,6 +126,10 @@ to prevent clashes with the watcher plugin * `watcher.interval.medium` is now `resource.reload.interval.medium` * `watcher.interval.high` is now `resource.reload.interval.high` +==== index.gateway setting renamed + +* `index.gateway.local.sync` is now `index.translog.sync_interval` + ==== Hunspell dictionary configuration The parameter `indices.analysis.hunspell.dictionary.location` has been From 61e4283a16cc12b9daf13e5ba76a34159b3355ba Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Fri, 29 Jan 2016 14:36:11 -0800 Subject: [PATCH 02/49] Add processor tags to on_failure metadata in ingest pipeline closes #16202 --- .../ingest/core/CompoundProcessor.java | 13 ++++++++----- .../ingest/core/CompoundProcessorTests.java | 17 ++++++++++------- docs/reference/ingest/ingest.asciidoc | 2 +- .../10_pipeline_with_mustache_templates.yaml | 5 +++-- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/ingest/core/CompoundProcessor.java b/core/src/main/java/org/elasticsearch/ingest/core/CompoundProcessor.java index 699720e18ca..c784ea1c57a 100644 --- a/core/src/main/java/org/elasticsearch/ingest/core/CompoundProcessor.java +++ b/core/src/main/java/org/elasticsearch/ingest/core/CompoundProcessor.java @@ -32,7 +32,8 @@ import java.util.Objects; */ public class CompoundProcessor implements Processor { static final String ON_FAILURE_MESSAGE_FIELD = "on_failure_message"; - static final String ON_FAILURE_PROCESSOR_FIELD = "on_failure_processor"; + static final String ON_FAILURE_PROCESSOR_TYPE_FIELD = "on_failure_processor_type"; + static final String ON_FAILURE_PROCESSOR_TAG_FIELD = "on_failure_processor_tag"; private final List processors; private final List onFailureProcessors; @@ -74,24 +75,26 @@ public class CompoundProcessor implements Processor { if (onFailureProcessors.isEmpty()) { throw e; } else { - executeOnFailure(ingestDocument, e, processor.getType()); + executeOnFailure(ingestDocument, e, processor.getType(), processor.getTag()); } break; } } } - void executeOnFailure(IngestDocument ingestDocument, Exception cause, String failedProcessorType) throws Exception { + void executeOnFailure(IngestDocument ingestDocument, Exception cause, String failedProcessorType, String failedProcessorTag) throws Exception { Map ingestMetadata = ingestDocument.getIngestMetadata(); try { ingestMetadata.put(ON_FAILURE_MESSAGE_FIELD, cause.getMessage()); - ingestMetadata.put(ON_FAILURE_PROCESSOR_FIELD, failedProcessorType); + ingestMetadata.put(ON_FAILURE_PROCESSOR_TYPE_FIELD, failedProcessorType); + ingestMetadata.put(ON_FAILURE_PROCESSOR_TAG_FIELD, failedProcessorTag); for (Processor processor : onFailureProcessors) { processor.execute(ingestDocument); } } finally { ingestMetadata.remove(ON_FAILURE_MESSAGE_FIELD); - ingestMetadata.remove(ON_FAILURE_PROCESSOR_FIELD); + ingestMetadata.remove(ON_FAILURE_PROCESSOR_TYPE_FIELD); + ingestMetadata.remove(ON_FAILURE_PROCESSOR_TAG_FIELD); } } } diff --git a/core/src/test/java/org/elasticsearch/ingest/core/CompoundProcessorTests.java b/core/src/test/java/org/elasticsearch/ingest/core/CompoundProcessorTests.java index f21644e6005..7bc8922af41 100644 --- a/core/src/test/java/org/elasticsearch/ingest/core/CompoundProcessorTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/core/CompoundProcessorTests.java @@ -80,9 +80,10 @@ public class CompoundProcessorTests extends ESTestCase { TestProcessor processor1 = new TestProcessor("id", "first", ingestDocument -> {throw new RuntimeException("error");}); TestProcessor processor2 = new TestProcessor(ingestDocument -> { Map ingestMetadata = ingestDocument.getIngestMetadata(); - assertThat(ingestMetadata.size(), equalTo(2)); + assertThat(ingestMetadata.size(), equalTo(3)); assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_MESSAGE_FIELD), equalTo("error")); - assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_FIELD), equalTo("first")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TYPE_FIELD), equalTo("first")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TAG_FIELD), equalTo("id")); }); CompoundProcessor compoundProcessor = new CompoundProcessor(Collections.singletonList(processor1), Collections.singletonList(processor2)); @@ -94,18 +95,20 @@ public class CompoundProcessorTests extends ESTestCase { public void testSingleProcessorWithNestedFailures() throws Exception { TestProcessor processor = new TestProcessor("id", "first", ingestDocument -> {throw new RuntimeException("error");}); - TestProcessor processorToFail = new TestProcessor("id", "second", ingestDocument -> { + TestProcessor processorToFail = new TestProcessor("id2", "second", ingestDocument -> { Map ingestMetadata = ingestDocument.getIngestMetadata(); - assertThat(ingestMetadata.size(), equalTo(2)); + assertThat(ingestMetadata.size(), equalTo(3)); assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_MESSAGE_FIELD), equalTo("error")); - assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_FIELD), equalTo("first")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TYPE_FIELD), equalTo("first")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TAG_FIELD), equalTo("id")); throw new RuntimeException("error"); }); TestProcessor lastProcessor = new TestProcessor(ingestDocument -> { Map ingestMetadata = ingestDocument.getIngestMetadata(); - assertThat(ingestMetadata.size(), equalTo(2)); + assertThat(ingestMetadata.size(), equalTo(3)); assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_MESSAGE_FIELD), equalTo("error")); - assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_FIELD), equalTo("second")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TYPE_FIELD), equalTo("second")); + assertThat(ingestMetadata.get(CompoundProcessor.ON_FAILURE_PROCESSOR_TAG_FIELD), equalTo("id2")); }); CompoundProcessor compoundOnFailProcessor = new CompoundProcessor(Collections.singletonList(processorToFail), Collections.singletonList(lastProcessor)); CompoundProcessor compoundProcessor = new CompoundProcessor(Collections.singletonList(processor), Collections.singletonList(compoundOnFailProcessor)); diff --git a/docs/reference/ingest/ingest.asciidoc b/docs/reference/ingest/ingest.asciidoc index e1ce35eb23c..dfe377ecac4 100644 --- a/docs/reference/ingest/ingest.asciidoc +++ b/docs/reference/ingest/ingest.asciidoc @@ -725,7 +725,7 @@ the index for which failed documents get sent. Sometimes you may want to retrieve the actual error message that was thrown by a failed processor. To do so you can access metadata fields called -`on_failure_message` and `on_failure_processor`. These fields are only accessible +`on_failure_message`, `on_failure_processor_type`, `on_failure_processor_tag`. These fields are only accessible from within the context of an `on_failure` block. Here is an updated version of our first example which leverages these fields to provide the error message instead of manually setting it. diff --git a/qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml b/qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml index 9e644773c6a..491e1dae292 100644 --- a/qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml +++ b/qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml @@ -185,12 +185,13 @@ "processors": [ { "remove" : { + "tag" : "first_processor", "field" : "field_to_remove", "on_failure" : [ { "set" : { "field" : "error", - "value" : "processor [{{ _ingest.on_failure_processor }}]: {{ _ingest.on_failure_message }}" + "value" : "processor {{ _ingest.on_failure_processor_tag }} [{{ _ingest.on_failure_processor_type }}]: {{ _ingest.on_failure_message }}" } } ] @@ -217,4 +218,4 @@ id: 1 - length: { _source: 2 } - match: { _source.do_nothing: "foo" } - - match: { _source.error: "processor [remove]: field [field_to_remove] not present as part of path [field_to_remove]" } + - match: { _source.error: "processor first_processor [remove]: field [field_to_remove] not present as part of path [field_to_remove]" } From b8f08c35ec832447581d7eb2757845d0ec80f1dc Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Fri, 29 Jan 2016 18:38:52 -0800 Subject: [PATCH 03/49] Plugin: Remove multicast plugin closes #16310. --- .projectile | 1 - .../elasticsearch/plugins/PluginManager.java | 1 - .../elasticsearch/plugins/plugin-install.help | 1 - dev-tools/smoke_test_rc.py | 1 - docs/plugins/discovery-multicast.asciidoc | 55 -- docs/plugins/discovery.asciidoc | 4 - docs/reference/migration/migrate_3_0.asciidoc | 4 + plugins/discovery-multicast/build.gradle | 23 - .../discovery/multicast/MulticastChannel.java | 390 ----------- .../multicast/MulticastDiscoveryPlugin.java | 61 -- .../discovery/multicast/MulticastZenPing.java | 604 ------------------ .../plugin-metadata/plugin-security.policy | 23 - .../multicast/MulticastDiscoveryRestIT.java | 41 -- .../multicast/MulticastZenPingTests.java | 192 ------ .../test/discovery_multicast/10_basic.yaml | 13 - .../plugins/PluginManagerTests.java | 1 - .../packaging/scripts/plugin_test_cases.bash | 8 - settings.gradle | 1 - 18 files changed, 4 insertions(+), 1420 deletions(-) delete mode 100644 docs/plugins/discovery-multicast.asciidoc delete mode 100644 plugins/discovery-multicast/build.gradle delete mode 100644 plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastChannel.java delete mode 100644 plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryPlugin.java delete mode 100644 plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPing.java delete mode 100644 plugins/discovery-multicast/src/main/plugin-metadata/plugin-security.policy delete mode 100644 plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryRestIT.java delete mode 100644 plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPingTests.java delete mode 100644 plugins/discovery-multicast/src/test/resources/rest-api-spec/test/discovery_multicast/10_basic.yaml diff --git a/.projectile b/.projectile index d2a5e762a88..49e2b292c26 100644 --- a/.projectile +++ b/.projectile @@ -16,7 +16,6 @@ -/plugins/discovery-azure/target -/plugins/discovery-ec2/target -/plugins/discovery-gce/target --/plugins/discovery-multicast/target -/plugins/jvm-example/target -/plugins/lang-expression/target -/plugins/lang-groovy/target diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginManager.java b/core/src/main/java/org/elasticsearch/plugins/PluginManager.java index a107c957bd4..044b4c9dbbd 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginManager.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginManager.java @@ -100,7 +100,6 @@ public class PluginManager { "discovery-azure", "discovery-ec2", "discovery-gce", - "discovery-multicast", "ingest-geoip", "lang-javascript", "lang-painless", diff --git a/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help b/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help index 9d5b8b3d68d..15efac60308 100644 --- a/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help +++ b/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help @@ -42,7 +42,6 @@ OFFICIAL PLUGINS - discovery-azure - discovery-ec2 - discovery-gce - - discovery-multicast - ingest-geoip - lang-javascript - lang-painless diff --git a/dev-tools/smoke_test_rc.py b/dev-tools/smoke_test_rc.py index 8c8a6fb9fae..f32a6b80f52 100644 --- a/dev-tools/smoke_test_rc.py +++ b/dev-tools/smoke_test_rc.py @@ -66,7 +66,6 @@ DEFAULT_PLUGINS = ["analysis-icu", "discovery-azure", "discovery-ec2", "discovery-gce", - "discovery-multicast", "lang-javascript", "lang-painless", "lang-python", diff --git a/docs/plugins/discovery-multicast.asciidoc b/docs/plugins/discovery-multicast.asciidoc deleted file mode 100644 index 75acbd89577..00000000000 --- a/docs/plugins/discovery-multicast.asciidoc +++ /dev/null @@ -1,55 +0,0 @@ -[[discovery-multicast]] -=== Multicast Discovery Plugin - -The Multicast Discovery plugin provides the ability to form a cluster using -TCP/IP multicast messages. - -[[discovery-multicast-install]] -[float] -==== Installation - -This plugin can be installed using the plugin manager: - -[source,sh] ----------------------------------------------------------------- -sudo bin/plugin install discovery-multicast ----------------------------------------------------------------- - -The plugin must be installed on every node in the cluster, and each node must -be restarted after installation. - -[[discovery-multicast-remove]] -[float] -==== Removal - -The plugin can be removed with the following command: - -[source,sh] ----------------------------------------------------------------- -sudo bin/plugin remove discovery-multicast ----------------------------------------------------------------- - -The node must be stopped before removing the plugin. - -[[discovery-multicast-usage]] -==== Configuring multicast discovery - -Multicast ping discovery of other nodes is done by sending one or more -multicast requests which existing nodes will receive and -respond to. It provides the following settings with the -`discovery.zen.ping.multicast` prefix: - -[cols="<,<",options="header",] -|======================================================================= -|Setting |Description -|`group` |The group address to use. Defaults to `224.2.2.4`. - -|`port` |The port to use. Defaults to `54328`. - -|`ttl` |The ttl of the multicast message. Defaults to `3`. - -|`address` |The address to bind to, defaults to `null` which means it -will bind `network.bind_host` - -|`enabled` |Whether multicast ping discovery is enabled. Defaults to `false`. -|======================================================================= diff --git a/docs/plugins/discovery.asciidoc b/docs/plugins/discovery.asciidoc index cfc98e45dee..f6f181bc1f1 100644 --- a/docs/plugins/discovery.asciidoc +++ b/docs/plugins/discovery.asciidoc @@ -21,10 +21,6 @@ The Azure discovery plugin uses the Azure API for unicast discovery. The Google Compute Engine discovery plugin uses the GCE API for unicast discovery. -<>:: - -The multicast plugin sends multicast messages to discover other nodes in the cluster. - [float] ==== Community contributed discovery plugins diff --git a/docs/reference/migration/migrate_3_0.asciidoc b/docs/reference/migration/migrate_3_0.asciidoc index 78f8ff40307..1574dbd128e 100644 --- a/docs/reference/migration/migrate_3_0.asciidoc +++ b/docs/reference/migration/migrate_3_0.asciidoc @@ -319,6 +319,10 @@ disable doc values is by using the `doc_values` property of mappings. Site plugins have been removed. It is recommended to migrate site plugins to Kibana plugins. +==== Multicast plugin removed + +Multicast has been removed. Use unicast discovery, or one of the cloud discovery plugins. + ==== Plugins with custom query implementations Plugins implementing custom queries need to implement the `fromXContent(QueryParseContext)` method in their diff --git a/plugins/discovery-multicast/build.gradle b/plugins/discovery-multicast/build.gradle deleted file mode 100644 index 295f28c094b..00000000000 --- a/plugins/discovery-multicast/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -esplugin { - description 'The Multicast Discovery plugin allows discovery other nodes using multicast requests' - classname 'org.elasticsearch.plugin.discovery.multicast.MulticastDiscoveryPlugin' -} diff --git a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastChannel.java b/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastChannel.java deleted file mode 100644 index dee74b9ddce..00000000000 --- a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastChannel.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugin.discovery.multicast; - -import org.apache.lucene.util.IOUtils; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; -import org.elasticsearch.common.settings.Settings; - -import java.io.Closeable; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; -import java.net.SocketAddress; -import java.net.SocketTimeoutException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.elasticsearch.common.util.concurrent.EsExecutors.daemonThreadFactory; - -/** - * A multicast channel that supports registering for receive events, and sending datagram packets. Allows - * to easily share the same multicast socket if it holds the same config. - */ -abstract class MulticastChannel implements Closeable { - - /** - * Builds a channel based on the provided config, allowing to control if sharing a channel that uses - * the same config is allowed or not. - */ - public static MulticastChannel getChannel(String name, boolean shared, Config config, Listener listener) throws Exception { - if (!shared) { - return new Plain(listener, name, config); - } - return Shared.getSharedChannel(listener, config); - } - - /** - * Config of multicast channel. - */ - public static final class Config { - public final int port; - public final String group; - public final int bufferSize; - public final int ttl; - public final InetAddress multicastInterface; - public final boolean deferToInterface; - - public Config(int port, String group, int bufferSize, int ttl, - InetAddress multicastInterface, boolean deferToInterface) { - this.port = port; - this.group = group; - this.bufferSize = bufferSize; - this.ttl = ttl; - this.multicastInterface = multicastInterface; - this.deferToInterface = deferToInterface; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Config config = (Config) o; - - if (bufferSize != config.bufferSize) return false; - if (port != config.port) return false; - if (ttl != config.ttl) return false; - if (group != null ? !group.equals(config.group) : config.group != null) return false; - if (multicastInterface != null ? !multicastInterface.equals(config.multicastInterface) : config.multicastInterface != null) - return false; - - return true; - } - - @Override - public int hashCode() { - int result = port; - result = 31 * result + (group != null ? group.hashCode() : 0); - result = 31 * result + bufferSize; - result = 31 * result + ttl; - result = 31 * result + (multicastInterface != null ? multicastInterface.hashCode() : 0); - return result; - } - } - - /** - * Listener that gets called when data is received on the multicast channel. - */ - public static interface Listener { - void onMessage(BytesReference data, SocketAddress address); - } - - /** - * Simple listener that wraps multiple listeners into one. - */ - public static class MultiListener implements Listener { - - private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); - - public void add(Listener listener) { - this.listeners.add(listener); - } - - public boolean remove(Listener listener) { - return this.listeners.remove(listener); - } - - @Override - public void onMessage(BytesReference data, SocketAddress address) { - for (Listener listener : listeners) { - listener.onMessage(data, address); - } - } - } - - protected final Listener listener; - private AtomicBoolean closed = new AtomicBoolean(); - - protected MulticastChannel(Listener listener) { - this.listener = listener; - } - - /** - * Send the data over the multicast channel. - */ - public abstract void send(BytesReference data) throws Exception; - - /** - * Close the channel. - */ - @Override - public void close() { - if (closed.compareAndSet(false, true)) { - close(listener); - } - } - - protected abstract void close(Listener listener); - - public static final String SHARED_CHANNEL_NAME = "#shared#"; - /** - * A shared channel that keeps a static map of Config -> Shared channels, and closes shared - * channel once their reference count has reached 0. It also handles de-registering relevant - * listener from the shared list of listeners. - */ - private final static class Shared extends MulticastChannel { - - private static final Map sharedChannels = new HashMap<>(); - private static final Object mutex = new Object(); // global mutex so we don't sync on static methods (.class) - - static MulticastChannel getSharedChannel(Listener listener, Config config) throws Exception { - - synchronized (mutex) { - Shared shared = sharedChannels.get(config); - if (shared != null) { - shared.incRef(); - ((MultiListener) shared.listener).add(listener); - } else { - MultiListener multiListener = new MultiListener(); - multiListener.add(listener); - shared = new Shared(multiListener, new Plain(multiListener, SHARED_CHANNEL_NAME, config)); - sharedChannels.put(config, shared); - } - return new Delegate(listener, shared); - } - } - - static void close(Shared shared, Listener listener) { - synchronized (mutex) { - // remove this - boolean removed = ((MultiListener) shared.listener).remove(listener); - assert removed : "a listener should be removed"; - if (shared.decRef() == 0) { - assert ((MultiListener) shared.listener).listeners.isEmpty(); - sharedChannels.remove(shared.channel.getConfig()); - shared.channel.close(); - } - } - } - - final Plain channel; - private int refCount = 1; - - Shared(MultiListener listener, Plain channel) { - super(listener); - this.channel = channel; - } - - private void incRef() { - refCount++; - } - - private int decRef() { - --refCount; - assert refCount >= 0 : "illegal ref counting, close called multiple times"; - return refCount; - } - - @Override - public void send(BytesReference data) throws Exception { - channel.send(data); - } - - @Override - public void close() { - assert false : "Shared references should never be closed directly, only via Delegate"; - } - - @Override - protected void close(Listener listener) { - close(this, listener); - } - } - - /** - * A light weight delegate that wraps another channel, mainly to support delegating - * the close method with the provided listener and not holding existing listener. - */ - private final static class Delegate extends MulticastChannel { - - private final MulticastChannel channel; - - Delegate(Listener listener, MulticastChannel channel) { - super(listener); - this.channel = channel; - } - - @Override - public void send(BytesReference data) throws Exception { - channel.send(data); - } - - @Override - protected void close(Listener listener) { - channel.close(listener); // we delegate here to the close with our listener, not with the delegate listener - } - } - - /** - * Simple implementation of a channel. - */ - @SuppressForbidden(reason = "I bind to wildcard addresses. I am a total nightmare") - private static class Plain extends MulticastChannel { - private final ESLogger logger; - private final Config config; - - private volatile MulticastSocket multicastSocket; - private final DatagramPacket datagramPacketSend; - private final DatagramPacket datagramPacketReceive; - - private final Object sendMutex = new Object(); - private final Object receiveMutex = new Object(); - - private final Receiver receiver; - private final Thread receiverThread; - - Plain(Listener listener, String name, Config config) throws Exception { - super(listener); - this.logger = ESLoggerFactory.getLogger(name); - this.config = config; - this.datagramPacketReceive = new DatagramPacket(new byte[config.bufferSize], config.bufferSize); - this.datagramPacketSend = new DatagramPacket(new byte[config.bufferSize], config.bufferSize, InetAddress.getByName(config.group), config.port); - this.multicastSocket = buildMulticastSocket(config); - this.receiver = new Receiver(); - this.receiverThread = daemonThreadFactory(Settings.builder().put("name", name).build(), "discovery#multicast#receiver").newThread(receiver); - this.receiverThread.start(); - } - - private MulticastSocket buildMulticastSocket(Config config) throws Exception { - SocketAddress addr = new InetSocketAddress(InetAddress.getByName(config.group), config.port); - MulticastSocket multicastSocket = new MulticastSocket(config.port); - try { - multicastSocket.setTimeToLive(config.ttl); - // OSX is not smart enough to tell that a socket bound to the - // 'lo0' interface needs to make sure to send the UDP packet - // out of the lo0 interface, so we need to do some special - // workarounds to fix it. - if (config.deferToInterface) { - // 'null' here tells the socket to deter to the interface set - // with .setInterface - multicastSocket.joinGroup(addr, null); - multicastSocket.setInterface(config.multicastInterface); - } else { - multicastSocket.setInterface(config.multicastInterface); - multicastSocket.joinGroup(InetAddress.getByName(config.group)); - } - multicastSocket.setReceiveBufferSize(config.bufferSize); - multicastSocket.setSendBufferSize(config.bufferSize); - multicastSocket.setSoTimeout(60000); - } catch (Throwable e) { - IOUtils.closeWhileHandlingException(multicastSocket); - throw e; - } - return multicastSocket; - } - - public Config getConfig() { - return this.config; - } - - @Override - public void send(BytesReference data) throws Exception { - synchronized (sendMutex) { - datagramPacketSend.setData(data.toBytes()); - multicastSocket.send(datagramPacketSend); - } - } - - @Override - protected void close(Listener listener) { - receiver.stop(); - receiverThread.interrupt(); - if (multicastSocket != null) { - IOUtils.closeWhileHandlingException(multicastSocket); - multicastSocket = null; - } - try { - receiverThread.join(10000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private class Receiver implements Runnable { - - private volatile boolean running = true; - - public void stop() { - running = false; - } - - @Override - public void run() { - while (running) { - try { - synchronized (receiveMutex) { - try { - multicastSocket.receive(datagramPacketReceive); - } catch (SocketTimeoutException ignore) { - continue; - } catch (Exception e) { - if (running) { - if (multicastSocket.isClosed()) { - logger.warn("multicast socket closed while running, restarting..."); - multicastSocket = buildMulticastSocket(config); - } else { - logger.warn("failed to receive packet, throttling...", e); - Thread.sleep(500); - } - } - continue; - } - } - if (datagramPacketReceive.getData().length > 0) { - listener.onMessage(new BytesArray(datagramPacketReceive.getData()), datagramPacketReceive.getSocketAddress()); - } - } catch (Throwable e) { - if (running) { - logger.warn("unexpected exception in multicast receiver", e); - } - } - } - } - } - } -} diff --git a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryPlugin.java b/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryPlugin.java deleted file mode 100644 index da9c5ba3c89..00000000000 --- a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugin.discovery.multicast; - -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.settings.SettingsModule; -import org.elasticsearch.discovery.DiscoveryModule; -import org.elasticsearch.plugins.Plugin; - -public class MulticastDiscoveryPlugin extends Plugin { - - private final Settings settings; - - public MulticastDiscoveryPlugin(Settings settings) { - this.settings = settings; - } - - @Override - public String name() { - return "discovery-multicast"; - } - - @Override - public String description() { - return "Multicast Discovery Plugin"; - } - - public void onModule(DiscoveryModule module) { - if (settings.getAsBoolean("discovery.zen.ping.multicast.enabled", false)) { - module.addZenPing(MulticastZenPing.class); - } - } - - public void onModule(SettingsModule module) { - module.registerSetting(MulticastZenPing.ADDRESS_SETTING); - module.registerSetting(MulticastZenPing.GROUP_SETTING); - module.registerSetting(MulticastZenPing.PORT_SETTING); - module.registerSetting(MulticastZenPing.SHARED_SETTING); - module.registerSetting(MulticastZenPing.TTL_SETTING); - module.registerSetting(MulticastZenPing.BUFFER_SIZE_SETTING); - module.registerSetting(MulticastZenPing.PING_ENABLED_SETTING); - module.registerSetting(MulticastZenPing.DEFERE_TO_INTERFACE_SETTING); - } -} diff --git a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPing.java b/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPing.java deleted file mode 100644 index 46f50235b58..00000000000 --- a/plugins/discovery-multicast/src/main/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPing.java +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugin.discovery.multicast; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.SocketAddress; -import java.security.AccessController; -import java.security.PrivilegedExceptionAction; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import org.apache.lucene.util.Constants; -import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.SpecialPermission; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.component.AbstractLifecycleComponent; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -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.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.AbstractRunnable; -import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.discovery.zen.ping.PingContextProvider; -import org.elasticsearch.discovery.zen.ping.ZenPing; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.EmptyTransportResponseHandler; -import org.elasticsearch.transport.TransportChannel; -import org.elasticsearch.transport.TransportException; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.transport.TransportRequestHandler; -import org.elasticsearch.transport.TransportResponse; -import org.elasticsearch.transport.TransportService; - -import com.carrotsearch.hppc.cursors.ObjectObjectCursor; - -import static org.elasticsearch.cluster.node.DiscoveryNode.readNode; -import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; -import static org.elasticsearch.common.util.concurrent.ConcurrentCollections.newConcurrentMap; - -/** - * - */ -public class MulticastZenPing extends AbstractLifecycleComponent implements ZenPing { - - public static final String ACTION_NAME = "internal:discovery/zen/multicast"; - - private static final byte[] INTERNAL_HEADER = new byte[]{1, 9, 8, 4}; - - private static final int PING_SIZE_ESTIMATE = 150; - - private final String address; - private final int port; - private final String group; - private final int bufferSize; - private final int ttl; - - private final ThreadPool threadPool; - private final TransportService transportService; - private final ClusterName clusterName; - private final NetworkService networkService; - private final Version version; - private volatile PingContextProvider contextProvider; - - private final boolean pingEnabled; - - private volatile MulticastChannel multicastChannel; - - private final AtomicInteger pingIdGenerator = new AtomicInteger(); - private final Map receivedResponses = newConcurrentMap(); - public static final Setting ADDRESS_SETTING = Setting.simpleString("discovery.zen.ping.multicast.address", false, Setting.Scope.CLUSTER); - public static final Setting PORT_SETTING = Setting.intSetting("discovery.zen.ping.multicast.port", 54328, 0, (1<<16)-1, false, Setting.Scope.CLUSTER); - public static final Setting GROUP_SETTING = new Setting<>("discovery.zen.ping.multicast.group", "224.2.2.4", Function.identity(), false, Setting.Scope.CLUSTER); - public static final Setting BUFFER_SIZE_SETTING = Setting.byteSizeSetting("discovery.zen.ping.multicast.buffer_size", new ByteSizeValue(2048, ByteSizeUnit.BYTES), false, Setting.Scope.CLUSTER); - public static final Setting TTL_SETTING = Setting.intSetting("discovery.zen.ping.multicast.ttl", 3, 0, 255, false, Setting.Scope.CLUSTER); - public static final Setting PING_ENABLED_SETTING = Setting.boolSetting("discovery.zen.ping.multicast.ping.enabled", true, false, Setting.Scope.CLUSTER); - public static final Setting SHARED_SETTING = Setting.boolSetting("discovery.zen.ping.multicast.shared", Constants.MAC_OS_X, false, Setting.Scope.CLUSTER); - public static final Setting DEFERE_TO_INTERFACE_SETTING = Setting.boolSetting("discovery.zen.ping.multicast.defer_group_to_set_interface", Constants.MAC_OS_X, false, Setting.Scope.CLUSTER); - - public MulticastZenPing(ThreadPool threadPool, TransportService transportService, ClusterName clusterName, Version version) { - this(EMPTY_SETTINGS, threadPool, transportService, clusterName, new NetworkService(EMPTY_SETTINGS), version); - } - - @Inject - public MulticastZenPing(Settings settings, ThreadPool threadPool, TransportService transportService, ClusterName clusterName, NetworkService networkService, Version version) { - super(settings); - this.threadPool = threadPool; - this.transportService = transportService; - this.clusterName = clusterName; - this.networkService = networkService; - this.version = version; - - this.address = ADDRESS_SETTING.exists(settings) ? ADDRESS_SETTING.get(settings) : null; - this.port = PORT_SETTING.get(settings); - this.group = GROUP_SETTING.get(settings); - this.bufferSize = BUFFER_SIZE_SETTING.get(settings).bytesAsInt(); - this.ttl = TTL_SETTING.get(settings); - this.pingEnabled = PING_ENABLED_SETTING.get(settings); - - logger.debug("using group [{}], with port [{}], ttl [{}], and address [{}]", group, port, ttl, address); - - this.transportService.registerRequestHandler(ACTION_NAME, MulticastPingResponse::new, ThreadPool.Names.SAME, new MulticastPingResponseRequestHandler()); - } - - @Override - public void setPingContextProvider(PingContextProvider nodesProvider) { - if (lifecycle.started()) { - throw new IllegalStateException("Can't set nodes provider when started"); - } - this.contextProvider = nodesProvider; - } - - @Override - protected void doStart() { - try { - // we know OSX has bugs in the JVM when creating multiple instances of multicast sockets - // causing for "socket close" exceptions when receive and/or crashes - boolean shared = SHARED_SETTING.get(settings); - // OSX does not correctly send multicasts FROM the right interface - boolean deferToInterface = DEFERE_TO_INTERFACE_SETTING.get(settings); - - final MulticastChannel.Config config = new MulticastChannel.Config(port, group, bufferSize, ttl, - getMulticastInterface(), deferToInterface); - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - multicastChannel = AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public MulticastChannel run() throws Exception { - return MulticastChannel.getChannel(nodeName(), shared, config, new Receiver()); - } - }); - } catch (Throwable t) { - String msg = "multicast failed to start [{}], disabling. Consider using IPv4 only (by defining env. variable `ES_USE_IPV4`)"; - logger.info(msg, t, ExceptionsHelper.detailedMessage(t)); - } - } - - - @SuppressWarnings("deprecation") // Used to support funky configuration options - private InetAddress getMulticastInterface() throws IOException { - // don't use publish address, the use case for that is e.g. a firewall or proxy and - // may not even be bound to an interface on this machine! use the first bound address. - List addresses = Arrays.asList(networkService.resolveBindHostAddresses(address == null ? null : new String[] { address })); - NetworkUtils.sortAddresses(addresses); - return addresses.get(0); - } - - @Override - protected void doStop() { - if (multicastChannel != null) { - multicastChannel.close(); - multicastChannel = null; - } - } - - @Override - protected void doClose() { - } - - public PingResponse[] pingAndWait(TimeValue timeout) { - final AtomicReference response = new AtomicReference<>(); - final CountDownLatch latch = new CountDownLatch(1); - try { - ping(new PingListener() { - @Override - public void onPing(PingResponse[] pings) { - response.set(pings); - latch.countDown(); - } - }, timeout); - } catch (EsRejectedExecutionException ex) { - logger.debug("Ping execution rejected", ex); - return PingResponse.EMPTY; - } - try { - latch.await(); - return response.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return PingResponse.EMPTY; - } - } - - @Override - public void ping(final PingListener listener, final TimeValue timeout) { - if (!pingEnabled || multicastChannel == null) { - threadPool.generic().execute(new Runnable() { - @Override - public void run() { - listener.onPing(PingResponse.EMPTY); - } - }); - return; - } - final int id = pingIdGenerator.incrementAndGet(); - try { - receivedResponses.put(id, new PingCollection()); - sendPingRequest(id); - // try and send another ping request halfway through (just in case someone woke up during it...) - // this can be a good trade-off to nailing the initial lookup or un-delivered messages - threadPool.schedule(TimeValue.timeValueMillis(timeout.millis() / 2), ThreadPool.Names.GENERIC, new AbstractRunnable() { - @Override - public void onFailure(Throwable t) { - logger.warn("[{}] failed to send second ping request", t, id); - finalizePingCycle(id, listener); - } - - @Override - public void doRun() { - sendPingRequest(id); - threadPool.schedule(TimeValue.timeValueMillis(timeout.millis() / 2), ThreadPool.Names.GENERIC, new AbstractRunnable() { - @Override - public void onFailure(Throwable t) { - logger.warn("[{}] failed to send third ping request", t, id); - finalizePingCycle(id, listener); - } - - @Override - public void doRun() { - // make one last ping, but finalize as soon as all nodes have responded or a timeout has past - PingCollection collection = receivedResponses.get(id); - FinalizingPingCollection finalizingPingCollection = new FinalizingPingCollection(id, collection, collection.size(), listener); - receivedResponses.put(id, finalizingPingCollection); - logger.trace("[{}] sending last pings", id); - sendPingRequest(id); - threadPool.schedule(TimeValue.timeValueMillis(timeout.millis() / 4), ThreadPool.Names.GENERIC, new AbstractRunnable() { - @Override - public void onFailure(Throwable t) { - logger.warn("[{}] failed to finalize ping", t, id); - } - - @Override - protected void doRun() throws Exception { - finalizePingCycle(id, listener); - } - }); - } - }); - } - }); - } catch (Exception e) { - logger.warn("failed to ping", e); - finalizePingCycle(id, listener); - } - } - - /** - * takes all pings collected for a given id and pass them to the given listener. - * this method is safe to call multiple times as is guaranteed to only finalize once. - */ - private void finalizePingCycle(int id, final PingListener listener) { - PingCollection responses = receivedResponses.remove(id); - if (responses != null) { - listener.onPing(responses.toArray()); - } - } - - private void sendPingRequest(int id) { - try { - BytesStreamOutput out = new BytesStreamOutput(PING_SIZE_ESTIMATE); - out.writeBytes(INTERNAL_HEADER); - // TODO: change to min_required version! - Version.writeVersion(version, out); - out.writeInt(id); - clusterName.writeTo(out); - contextProvider.nodes().localNode().writeTo(out); - out.close(); - multicastChannel.send(out.bytes()); - if (logger.isTraceEnabled()) { - logger.trace("[{}] sending ping request", id); - } - } catch (Exception e) { - if (lifecycle.stoppedOrClosed()) { - return; - } - if (logger.isDebugEnabled()) { - logger.debug("failed to send multicast ping request", e); - } else { - logger.warn("failed to send multicast ping request: {}", ExceptionsHelper.detailedMessage(e)); - } - } - } - - class FinalizingPingCollection extends PingCollection { - final private PingCollection internalCollection; - final private int expectedResponses; - final private AtomicInteger responseCount; - final private PingListener listener; - final private int id; - - public FinalizingPingCollection(int id, PingCollection internalCollection, int expectedResponses, PingListener listener) { - this.id = id; - this.internalCollection = internalCollection; - this.expectedResponses = expectedResponses; - this.responseCount = new AtomicInteger(); - this.listener = listener; - } - - @Override - public synchronized boolean addPing(PingResponse ping) { - if (internalCollection.addPing(ping)) { - if (responseCount.incrementAndGet() >= expectedResponses) { - logger.trace("[{}] all nodes responded", id); - finish(); - } - return true; - } - return false; - } - - @Override - public synchronized void addPings(PingResponse[] pings) { - internalCollection.addPings(pings); - } - - @Override - public synchronized PingResponse[] toArray() { - return internalCollection.toArray(); - } - - void finish() { - // spawn another thread as we may be running on a network thread - threadPool.generic().execute(new AbstractRunnable() { - @Override - public void onFailure(Throwable t) { - logger.error("failed to call ping listener", t); - } - - @Override - protected void doRun() throws Exception { - finalizePingCycle(id, listener); - } - }); - } - } - - class MulticastPingResponseRequestHandler implements TransportRequestHandler { - @Override - public void messageReceived(MulticastPingResponse request, TransportChannel channel) throws Exception { - if (logger.isTraceEnabled()) { - logger.trace("[{}] received {}", request.id, request.pingResponse); - } - PingCollection responses = receivedResponses.get(request.id); - if (responses == null) { - logger.warn("received ping response {} with no matching id [{}]", request.pingResponse, request.id); - } else { - responses.addPing(request.pingResponse); - } - channel.sendResponse(TransportResponse.Empty.INSTANCE); - } - } - - public static class MulticastPingResponse extends TransportRequest { - - int id; - - PingResponse pingResponse; - - public MulticastPingResponse() { - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - id = in.readInt(); - pingResponse = PingResponse.readPingResponse(in); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeInt(id); - pingResponse.writeTo(out); - } - } - - - private class Receiver implements MulticastChannel.Listener { - - @Override - public void onMessage(BytesReference data, SocketAddress address) { - int id = -1; - DiscoveryNode requestingNodeX = null; - ClusterName clusterName = null; - - Map externalPingData = null; - XContentType xContentType = null; - - try { - boolean internal = false; - if (data.length() > 4) { - int counter = 0; - for (; counter < INTERNAL_HEADER.length; counter++) { - if (data.get(counter) != INTERNAL_HEADER[counter]) { - break; - } - } - if (counter == INTERNAL_HEADER.length) { - internal = true; - } - } - if (internal) { - StreamInput input = StreamInput.wrap(new BytesArray(data.toBytes(), INTERNAL_HEADER.length, data.length() - INTERNAL_HEADER.length)); - Version version = Version.readVersion(input); - input.setVersion(version); - id = input.readInt(); - clusterName = ClusterName.readClusterName(input); - requestingNodeX = readNode(input); - } else { - xContentType = XContentFactory.xContentType(data); - if (xContentType != null) { - // an external ping - try (XContentParser parser = XContentFactory.xContent(xContentType).createParser(data)) { - externalPingData = parser.map(); - } - } else { - throw new IllegalStateException("failed multicast message, probably message from previous version"); - } - } - if (externalPingData != null) { - handleExternalPingRequest(externalPingData, xContentType, address); - } else { - handleNodePingRequest(id, requestingNodeX, clusterName); - } - } catch (Exception e) { - if (!lifecycle.started() || (e instanceof EsRejectedExecutionException)) { - logger.debug("failed to read requesting data from {}", e, address); - } else { - logger.warn("failed to read requesting data from {}", e, address); - } - } - } - - @SuppressWarnings("unchecked") - private void handleExternalPingRequest(Map externalPingData, XContentType contentType, SocketAddress remoteAddress) { - if (externalPingData.containsKey("response")) { - // ignoring responses sent over the multicast channel - logger.trace("got an external ping response (ignoring) from {}, content {}", remoteAddress, externalPingData); - return; - } - - if (multicastChannel == null) { - logger.debug("can't send ping response, no socket, from {}, content {}", remoteAddress, externalPingData); - return; - } - - Map request = (Map) externalPingData.get("request"); - if (request == null) { - logger.warn("malformed external ping request, no 'request' element from {}, content {}", remoteAddress, externalPingData); - return; - } - - final String requestClusterName = request.containsKey("cluster_name") ? request.get("cluster_name").toString() : request.containsKey("clusterName") ? request.get("clusterName").toString() : null; - if (requestClusterName == null) { - logger.warn("malformed external ping request, missing 'cluster_name' element within request, from {}, content {}", remoteAddress, externalPingData); - return; - } - - if (!requestClusterName.equals(clusterName.value())) { - logger.trace("got request for cluster_name {}, but our cluster_name is {}, from {}, content {}", - requestClusterName, clusterName.value(), remoteAddress, externalPingData); - return; - } - if (logger.isTraceEnabled()) { - logger.trace("got external ping request from {}, content {}", remoteAddress, externalPingData); - } - - try { - DiscoveryNode localNode = contextProvider.nodes().localNode(); - - XContentBuilder builder = XContentFactory.contentBuilder(contentType); - builder.startObject().startObject("response"); - builder.field("cluster_name", clusterName.value()); - builder.startObject("version").field("number", version.number()).field("snapshot_build", version.snapshot).endObject(); - builder.field("transport_address", localNode.address().toString()); - - if (contextProvider.nodeService() != null) { - for (Map.Entry attr : contextProvider.nodeService().attributes().entrySet()) { - builder.field(attr.getKey(), attr.getValue()); - } - } - - builder.startObject("attributes"); - for (ObjectObjectCursor attr : localNode.attributes()) { - builder.field(attr.key, attr.value); - } - builder.endObject(); - - builder.endObject().endObject(); - multicastChannel.send(builder.bytes()); - if (logger.isTraceEnabled()) { - logger.trace("sending external ping response {}", builder.string()); - } - } catch (Exception e) { - logger.warn("failed to send external multicast response", e); - } - } - - private void handleNodePingRequest(int id, DiscoveryNode requestingNodeX, ClusterName requestClusterName) { - if (!pingEnabled || multicastChannel == null) { - return; - } - final DiscoveryNodes discoveryNodes = contextProvider.nodes(); - final DiscoveryNode requestingNode = requestingNodeX; - if (requestingNode.id().equals(discoveryNodes.localNodeId())) { - // that's me, ignore - return; - } - if (!requestClusterName.equals(clusterName)) { - if (logger.isTraceEnabled()) { - logger.trace("[{}] received ping_request from [{}], but wrong cluster_name [{}], expected [{}], ignoring", - id, requestingNode, requestClusterName.value(), clusterName.value()); - } - return; - } - // don't connect between two client nodes, no need for that... - if (!discoveryNodes.localNode().shouldConnectTo(requestingNode)) { - if (logger.isTraceEnabled()) { - logger.trace("[{}] received ping_request from [{}], both are client nodes, ignoring", id, requestingNode, requestClusterName); - } - return; - } - final MulticastPingResponse multicastPingResponse = new MulticastPingResponse(); - multicastPingResponse.id = id; - multicastPingResponse.pingResponse = new PingResponse(discoveryNodes.localNode(), discoveryNodes.masterNode(), clusterName, contextProvider.nodeHasJoinedClusterOnce()); - - if (logger.isTraceEnabled()) { - logger.trace("[{}] received ping_request from [{}], sending {}", id, requestingNode, multicastPingResponse.pingResponse); - } - - if (!transportService.nodeConnected(requestingNode)) { - // do the connect and send on a thread pool - threadPool.generic().execute(new Runnable() { - @Override - public void run() { - // connect to the node if possible - try { - transportService.connectToNode(requestingNode); - transportService.sendRequest(requestingNode, ACTION_NAME, multicastPingResponse, new EmptyTransportResponseHandler(ThreadPool.Names.SAME) { - @Override - public void handleException(TransportException exp) { - logger.warn("failed to receive confirmation on sent ping response to [{}]", exp, requestingNode); - } - }); - } catch (Exception e) { - if (lifecycle.started()) { - logger.warn("failed to connect to requesting node {}", e, requestingNode); - } - } - } - }); - } else { - transportService.sendRequest(requestingNode, ACTION_NAME, multicastPingResponse, new EmptyTransportResponseHandler(ThreadPool.Names.SAME) { - @Override - public void handleException(TransportException exp) { - if (lifecycle.started()) { - logger.warn("failed to receive confirmation on sent ping response to [{}]", exp, requestingNode); - } - } - }); - } - } - } -} diff --git a/plugins/discovery-multicast/src/main/plugin-metadata/plugin-security.policy b/plugins/discovery-multicast/src/main/plugin-metadata/plugin-security.policy deleted file mode 100644 index 5752c86bb4f..00000000000 --- a/plugins/discovery-multicast/src/main/plugin-metadata/plugin-security.policy +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -grant { - // needed to bind multicast to arbitrary port - permission java.net.SocketPermission "localhost:1024-", "listen,resolve"; -}; diff --git a/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryRestIT.java b/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryRestIT.java deleted file mode 100644 index c6af20c011e..00000000000 --- a/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastDiscoveryRestIT.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugin.discovery.multicast; - -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.elasticsearch.test.rest.ESRestTestCase; -import org.elasticsearch.test.rest.RestTestCandidate; -import org.elasticsearch.test.rest.parser.RestTestParseException; - -import java.io.IOException; - -public class MulticastDiscoveryRestIT extends ESRestTestCase { - - public MulticastDiscoveryRestIT(@Name("yaml") RestTestCandidate testCandidate) { - super(testCandidate); - } - - @ParametersFactory - public static Iterable parameters() throws IOException, RestTestParseException { - return ESRestTestCase.createParameters(0, 1); - } -} - diff --git a/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPingTests.java b/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPingTests.java deleted file mode 100644 index 8c2d95ec799..00000000000 --- a/plugins/discovery-multicast/src/test/java/org/elasticsearch/plugin/discovery/multicast/MulticastZenPingTests.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugin.discovery.multicast; - -import org.apache.lucene.util.Constants; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.logging.Loggers; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.discovery.zen.ping.PingContextProvider; -import org.elasticsearch.discovery.zen.ping.ZenPing; -import org.elasticsearch.node.service.NodeService; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.transport.local.LocalTransport; -import org.hamcrest.Matchers; -import org.junit.Assert; - -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.MulticastSocket; - -public class MulticastZenPingTests extends ESTestCase { - - private Settings buildRandomMulticast(Settings settings) { - Settings.Builder builder = Settings.builder().put(settings); - builder.put("discovery.zen.ping.multicast.group", "224.2.3." + randomIntBetween(0, 255)); - builder.put("discovery.zen.ping.multicast.port", randomIntBetween(55000, 56000)); - builder.put("discovery.zen.ping.multicast.enabled", true); - if (randomBoolean()) { - builder.put("discovery.zen.ping.multicast.shared", randomBoolean()); - } - return builder.build(); - } - - public void testSimplePings() throws InterruptedException { - assumeTrue("https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193246", Constants.FREE_BSD == false); - Settings settings = Settings.EMPTY; - settings = buildRandomMulticast(settings); - Thread.sleep(30000); - - ThreadPool threadPool = new ThreadPool("testSimplePings"); - final ClusterName clusterName = new ClusterName("test"); - final TransportService transportServiceA = new TransportService(new LocalTransport(settings, threadPool, Version.CURRENT, new NamedWriteableRegistry()), threadPool).start(); - final DiscoveryNode nodeA = new DiscoveryNode("A", transportServiceA.boundAddress().publishAddress(), Version.CURRENT); - - final TransportService transportServiceB = new TransportService(new LocalTransport(settings, threadPool, Version.CURRENT, new NamedWriteableRegistry()), threadPool).start(); - final DiscoveryNode nodeB = new DiscoveryNode("B", transportServiceB.boundAddress().publishAddress(), Version.CURRENT); - - MulticastZenPing zenPingA = new MulticastZenPing(threadPool, transportServiceA, clusterName, Version.CURRENT); - zenPingA.setPingContextProvider(new PingContextProvider() { - @Override - public DiscoveryNodes nodes() { - return DiscoveryNodes.builder().put(nodeA).localNodeId("A").build(); - } - - @Override - public NodeService nodeService() { - return null; - } - - @Override - public boolean nodeHasJoinedClusterOnce() { - return false; - } - }); - zenPingA.start(); - - MulticastZenPing zenPingB = new MulticastZenPing(threadPool, transportServiceB, clusterName, Version.CURRENT); - zenPingB.setPingContextProvider(new PingContextProvider() { - @Override - public DiscoveryNodes nodes() { - return DiscoveryNodes.builder().put(nodeB).localNodeId("B").build(); - } - - @Override - public NodeService nodeService() { - return null; - } - - @Override - public boolean nodeHasJoinedClusterOnce() { - return true; - } - }); - zenPingB.start(); - - try { - logger.info("ping from A"); - ZenPing.PingResponse[] pingResponses = zenPingA.pingAndWait(TimeValue.timeValueSeconds(1)); - Assert.assertThat(pingResponses.length, Matchers.equalTo(1)); - Assert.assertThat(pingResponses[0].node().id(), Matchers.equalTo("B")); - Assert.assertTrue(pingResponses[0].hasJoinedOnce()); - - logger.info("ping from B"); - pingResponses = zenPingB.pingAndWait(TimeValue.timeValueSeconds(1)); - Assert.assertThat(pingResponses.length, Matchers.equalTo(1)); - Assert.assertThat(pingResponses[0].node().id(), Matchers.equalTo("A")); - Assert.assertFalse(pingResponses[0].hasJoinedOnce()); - - } finally { - zenPingA.close(); - zenPingB.close(); - transportServiceA.close(); - transportServiceB.close(); - terminate(threadPool); - } - } - - // This test is here because when running on FreeBSD, if no tests are - // executed for the 'multicast' project it will assume everything - // failed, so we need to have at least one test that runs. - public void testAlwaysRun() throws Exception { - assertTrue(true); - } - - @SuppressForbidden(reason = "I bind to wildcard addresses. I am a total nightmare") - public void testExternalPing() throws Exception { - assumeTrue("https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193246", Constants.FREE_BSD == false); - Settings settings = Settings.EMPTY; - settings = buildRandomMulticast(settings); - - final ThreadPool threadPool = new ThreadPool("testExternalPing"); - final ClusterName clusterName = new ClusterName("test"); - final TransportService transportServiceA = new TransportService(new LocalTransport(settings, threadPool, Version.CURRENT, new NamedWriteableRegistry()), threadPool).start(); - final DiscoveryNode nodeA = new DiscoveryNode("A", transportServiceA.boundAddress().publishAddress(), Version.CURRENT); - - MulticastZenPing zenPingA = new MulticastZenPing(threadPool, transportServiceA, clusterName, Version.CURRENT); - zenPingA.setPingContextProvider(new PingContextProvider() { - @Override - public DiscoveryNodes nodes() { - return DiscoveryNodes.builder().put(nodeA).localNodeId("A").build(); - } - - @Override - public NodeService nodeService() { - return null; - } - - @Override - public boolean nodeHasJoinedClusterOnce() { - return false; - } - }); - zenPingA.start(); - - MulticastSocket multicastSocket = null; - try { - Loggers.getLogger(MulticastZenPing.class).setLevel("TRACE"); - multicastSocket = new MulticastSocket(); - multicastSocket.setReceiveBufferSize(2048); - multicastSocket.setSendBufferSize(2048); - multicastSocket.setSoTimeout(60000); - - DatagramPacket datagramPacket = new DatagramPacket(new byte[2048], 2048, InetAddress.getByName("224.2.2.4"), 54328); - XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("request").field("cluster_name", "test").endObject().endObject(); - datagramPacket.setData(builder.bytes().toBytes()); - multicastSocket.send(datagramPacket); - Thread.sleep(100); - } finally { - Loggers.getLogger(MulticastZenPing.class).setLevel("INFO"); - if (multicastSocket != null) multicastSocket.close(); - zenPingA.close(); - terminate(threadPool); - } - } -} diff --git a/plugins/discovery-multicast/src/test/resources/rest-api-spec/test/discovery_multicast/10_basic.yaml b/plugins/discovery-multicast/src/test/resources/rest-api-spec/test/discovery_multicast/10_basic.yaml deleted file mode 100644 index 36172fa2c33..00000000000 --- a/plugins/discovery-multicast/src/test/resources/rest-api-spec/test/discovery_multicast/10_basic.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Integration tests for multicast discovery -# -"Multicast discovery loaded": - - do: - cluster.state: {} - - # Get master node id - - set: { master_node: master } - - - do: - nodes.info: {} - - - match: { nodes.$master.plugins.0.name: discovery-multicast } diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java index d997a167541..b8a8c4b39dd 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java @@ -598,7 +598,6 @@ public class PluginManagerTests extends ESIntegTestCase { PluginManager.checkForOfficialPlugins("mapper-attachments"); PluginManager.checkForOfficialPlugins("mapper-murmur3"); PluginManager.checkForOfficialPlugins("mapper-size"); - PluginManager.checkForOfficialPlugins("discovery-multicast"); PluginManager.checkForOfficialPlugins("discovery-azure"); PluginManager.checkForOfficialPlugins("discovery-ec2"); PluginManager.checkForOfficialPlugins("discovery-gce"); diff --git a/qa/vagrant/src/test/resources/packaging/scripts/plugin_test_cases.bash b/qa/vagrant/src/test/resources/packaging/scripts/plugin_test_cases.bash index 9889048e973..18a93711ff9 100644 --- a/qa/vagrant/src/test/resources/packaging/scripts/plugin_test_cases.bash +++ b/qa/vagrant/src/test/resources/packaging/scripts/plugin_test_cases.bash @@ -219,10 +219,6 @@ fi install_and_check_plugin discovery ec2 aws-java-sdk-core-*.jar } -@test "[$GROUP] install multicast discovery plugin" { - install_and_check_plugin discovery multicast -} - @test "[$GROUP] install lang-expression plugin" { install_and_check_plugin lang expression } @@ -325,10 +321,6 @@ fi remove_plugin discovery-ec2 } -@test "[$GROUP] remove multicast discovery plugin" { - remove_plugin discovery-multicast -} - @test "[$GROUP] remove lang-expression plugin" { remove_plugin lang-expression } diff --git a/settings.gradle b/settings.gradle index c8616789569..f7a7fc71772 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,7 +24,6 @@ List projects = [ 'plugins:discovery-azure', 'plugins:discovery-ec2', 'plugins:discovery-gce', - 'plugins:discovery-multicast', 'plugins:ingest-geoip', 'plugins:lang-javascript', 'plugins:lang-painless', From a398ba839cbaa44d9e357dae53243b1dc2ef75d8 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Sun, 31 Jan 2016 09:58:41 -0800 Subject: [PATCH 04/49] Tests: Make single node utility methods non-static With the recent change to allow ESSingleNodeTestCase to specify plugins and Version for the node, node creation became lazy, where it now happens before the first test to run. However, there were many static methods on ESSingleNodeTestCase that still try to access the node. This change makes those methods non-static. --- .../common/util/BigArraysTests.java | 4 ++-- .../common/util/BytesRefHashTests.java | 9 ++++++++- .../common/util/LongHashTests.java | 9 ++++++++- .../common/util/LongObjectHashMapTests.java | 10 +++++++++- .../threadpool/SimpleThreadPoolIT.java | 2 +- .../test/ESSingleNodeTestCase.java | 20 +++++++++---------- 6 files changed, 38 insertions(+), 16 deletions(-) diff --git a/core/src/test/java/org/elasticsearch/common/util/BigArraysTests.java b/core/src/test/java/org/elasticsearch/common/util/BigArraysTests.java index bf55a330509..1735515bf3a 100644 --- a/core/src/test/java/org/elasticsearch/common/util/BigArraysTests.java +++ b/core/src/test/java/org/elasticsearch/common/util/BigArraysTests.java @@ -37,8 +37,8 @@ import java.util.Arrays; public class BigArraysTests extends ESSingleNodeTestCase { - public static BigArrays randombigArrays() { - final PageCacheRecycler recycler = randomBoolean() ? null : ESSingleNodeTestCase.getInstanceFromNode(PageCacheRecycler.class); + private BigArrays randombigArrays() { + final PageCacheRecycler recycler = randomBoolean() ? null : getInstanceFromNode(PageCacheRecycler.class); return new MockBigArrays(recycler, new NoneCircuitBreakerService()); } diff --git a/core/src/test/java/org/elasticsearch/common/util/BytesRefHashTests.java b/core/src/test/java/org/elasticsearch/common/util/BytesRefHashTests.java index a26a06a09a3..01c27a65ab8 100644 --- a/core/src/test/java/org/elasticsearch/common/util/BytesRefHashTests.java +++ b/core/src/test/java/org/elasticsearch/common/util/BytesRefHashTests.java @@ -25,6 +25,8 @@ import com.carrotsearch.hppc.cursors.ObjectLongCursor; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; import org.apache.lucene.util.TestUtil; +import org.elasticsearch.cache.recycler.PageCacheRecycler; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.test.ESSingleNodeTestCase; import java.util.HashMap; @@ -38,13 +40,18 @@ public class BytesRefHashTests extends ESSingleNodeTestCase { BytesRefHash hash; + private BigArrays randombigArrays() { + final PageCacheRecycler recycler = randomBoolean() ? null : getInstanceFromNode(PageCacheRecycler.class); + return new MockBigArrays(recycler, new NoneCircuitBreakerService()); + } + private void newHash() { if (hash != null) { hash.close(); } // Test high load factors to make sure that collision resolution works fine final float maxLoadFactor = 0.6f + randomFloat() * 0.39f; - hash = new BytesRefHash(randomIntBetween(0, 100), maxLoadFactor, BigArraysTests.randombigArrays()); + hash = new BytesRefHash(randomIntBetween(0, 100), maxLoadFactor, randombigArrays()); } @Override diff --git a/core/src/test/java/org/elasticsearch/common/util/LongHashTests.java b/core/src/test/java/org/elasticsearch/common/util/LongHashTests.java index f5ae388db77..9439044a7be 100644 --- a/core/src/test/java/org/elasticsearch/common/util/LongHashTests.java +++ b/core/src/test/java/org/elasticsearch/common/util/LongHashTests.java @@ -22,6 +22,8 @@ package org.elasticsearch.common.util; import com.carrotsearch.hppc.LongLongHashMap; import com.carrotsearch.hppc.LongLongMap; import com.carrotsearch.hppc.cursors.LongLongCursor; +import org.elasticsearch.cache.recycler.PageCacheRecycler; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.test.ESSingleNodeTestCase; import java.util.HashMap; @@ -33,6 +35,11 @@ import java.util.Set; public class LongHashTests extends ESSingleNodeTestCase { LongHash hash; + private BigArrays randombigArrays() { + final PageCacheRecycler recycler = randomBoolean() ? null : getInstanceFromNode(PageCacheRecycler.class); + return new MockBigArrays(recycler, new NoneCircuitBreakerService()); + } + private void newHash() { if (hash != null) { hash.close(); @@ -40,7 +47,7 @@ public class LongHashTests extends ESSingleNodeTestCase { // Test high load factors to make sure that collision resolution works fine final float maxLoadFactor = 0.6f + randomFloat() * 0.39f; - hash = new LongHash(randomIntBetween(0, 100), maxLoadFactor, BigArraysTests.randombigArrays()); + hash = new LongHash(randomIntBetween(0, 100), maxLoadFactor, randombigArrays()); } @Override diff --git a/core/src/test/java/org/elasticsearch/common/util/LongObjectHashMapTests.java b/core/src/test/java/org/elasticsearch/common/util/LongObjectHashMapTests.java index bf091828ca5..1775f86199a 100644 --- a/core/src/test/java/org/elasticsearch/common/util/LongObjectHashMapTests.java +++ b/core/src/test/java/org/elasticsearch/common/util/LongObjectHashMapTests.java @@ -20,12 +20,20 @@ package org.elasticsearch.common.util; import com.carrotsearch.hppc.LongObjectHashMap; +import org.elasticsearch.cache.recycler.PageCacheRecycler; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.test.ESSingleNodeTestCase; public class LongObjectHashMapTests extends ESSingleNodeTestCase { + + private BigArrays randombigArrays() { + final PageCacheRecycler recycler = randomBoolean() ? null : getInstanceFromNode(PageCacheRecycler.class); + return new MockBigArrays(recycler, new NoneCircuitBreakerService()); + } + public void testDuel() { final LongObjectHashMap map1 = new LongObjectHashMap<>(); - final LongObjectPagedHashMap map2 = new LongObjectPagedHashMap<>(randomInt(42), 0.6f + randomFloat() * 0.39f, BigArraysTests.randombigArrays()); + final LongObjectPagedHashMap map2 = new LongObjectPagedHashMap<>(randomInt(42), 0.6f + randomFloat() * 0.39f, randombigArrays()); final int maxKey = randomIntBetween(1, 10000); final int iters = scaledRandomIntBetween(10000, 100000); for (int i = 0; i < iters; ++i) { diff --git a/core/src/test/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java b/core/src/test/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java index ea225d9680b..e36ac662342 100644 --- a/core/src/test/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java +++ b/core/src/test/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java @@ -115,7 +115,7 @@ public class SimpleThreadPoolIT extends ESIntegTestCase { for (String threadName : threadNames) { // ignore some shared threads we know that are created within the same VM, like the shared discovery one // or the ones that are occasionally come up from ESSingleNodeTestCase - if (threadName.contains("[" + ESSingleNodeTestCase.nodeName() + "]") + if (threadName.contains("[node_s_0]") // TODO: this can't possibly be right! single node and integ test are unrelated! || threadName.contains("Keep-Alive-Timer")) { continue; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java index f2f1d19bc9f..189b4e8092c 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java @@ -183,49 +183,49 @@ public abstract class ESSingleNodeTestCase extends ESTestCase { /** * Returns a client to the single-node cluster. */ - public static Client client() { + public Client client() { return NODE.client(); } /** * Returns the single test nodes name. */ - public static String nodeName() { + public String nodeName() { return "node_s_0"; } /** * Return a reference to the singleton node. */ - protected static Node node() { + protected Node node() { return NODE; } /** * Get an instance for a particular class using the injector of the singleton node. */ - protected static T getInstanceFromNode(Class clazz) { + protected T getInstanceFromNode(Class clazz) { return NODE.injector().getInstance(clazz); } /** * Create a new index on the singleton node with empty index settings. */ - protected static IndexService createIndex(String index) { + protected IndexService createIndex(String index) { return createIndex(index, Settings.EMPTY); } /** * Create a new index on the singleton node with the provided index settings. */ - protected static IndexService createIndex(String index, Settings settings) { + protected IndexService createIndex(String index, Settings settings) { return createIndex(index, settings, null, (XContentBuilder) null); } /** * Create a new index on the singleton node with the provided index settings. */ - protected static IndexService createIndex(String index, Settings settings, String type, XContentBuilder mappings) { + protected IndexService createIndex(String index, Settings settings, String type, XContentBuilder mappings) { CreateIndexRequestBuilder createIndexRequestBuilder = client().admin().indices().prepareCreate(index).setSettings(settings); if (type != null && mappings != null) { createIndexRequestBuilder.addMapping(type, mappings); @@ -236,7 +236,7 @@ public abstract class ESSingleNodeTestCase extends ESTestCase { /** * Create a new index on the singleton node with the provided index settings. */ - protected static IndexService createIndex(String index, Settings settings, String type, Object... mappings) { + protected IndexService createIndex(String index, Settings settings, String type, Object... mappings) { CreateIndexRequestBuilder createIndexRequestBuilder = client().admin().indices().prepareCreate(index).setSettings(settings); if (type != null && mappings != null) { createIndexRequestBuilder.addMapping(type, mappings); @@ -244,7 +244,7 @@ public abstract class ESSingleNodeTestCase extends ESTestCase { return createIndex(index, createIndexRequestBuilder); } - protected static IndexService createIndex(String index, CreateIndexRequestBuilder createIndexRequestBuilder) { + protected IndexService createIndex(String index, CreateIndexRequestBuilder createIndexRequestBuilder) { assertAcked(createIndexRequestBuilder.get()); // Wait for the index to be allocated so that cluster state updates don't override // changes that would have been done locally @@ -259,7 +259,7 @@ public abstract class ESSingleNodeTestCase extends ESTestCase { /** * Create a new search context. */ - protected static SearchContext createSearchContext(IndexService indexService) { + protected SearchContext createSearchContext(IndexService indexService) { BigArrays bigArrays = indexService.getIndexServices().getBigArrays(); ThreadPool threadPool = indexService.getIndexServices().getThreadPool(); PageCacheRecycler pageCacheRecycler = node().injector().getInstance(PageCacheRecycler.class); From a052dfeb3891545e983bcb7f2b66a83cdbfbfa9e Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Sun, 31 Jan 2016 17:22:56 -0800 Subject: [PATCH 05/49] Plugins: Reduce complexity of plugin cli The plugin cli currently is extremely lenient, allowing most errors to simply be logged. This can lead to either corrupt installations (eg partially installed plugins), or confused users. This change rewrites the plugin cli to have almost no leniency. Unfortunately it was not possible to remove all leniency, due in particular to how config files are handled. The following functionality was simplified: * The format of the name argument to install a plugin is now an official plugin name, maven coordinates, or a URL. * Checksum files are required, and only checked, for official plugins and maven plugins. Checksums are also only SHA1. * Downloading no longer uses a separate thread, and no longer has a timeout. * Installation, and removal, attempts to be atomic. This only truly works when no config or bin files exist. * config and bin directories are verified before copying is attempted. * Permissions and user/group are no longer set on config and bin files. We rely on the users umask. * config and bin directories must only contain files, no subdirectories. * The code is reorganized so each command is a separate class. These classes already existed, but were embedded in the plugin cli class, as an extra layer between the cli code and the code running for each command. --- .../elasticsearch/common/cli/HelpPrinter.java | 2 +- .../elasticsearch/common/cli/Terminal.java | 12 - .../http/client/HttpDownloadHelper.java | 488 ------------ .../common/io/FileSystemUtils.java | 188 ----- .../logging/log4j/TerminalAppender.java | 2 +- .../plugins/InstallPluginCommand.java | 402 ++++++++++ .../plugins/ListPluginsCommand.java | 56 ++ .../org/elasticsearch/plugins/PluginCli.java | 124 +++ .../org/elasticsearch/plugins/PluginInfo.java | 1 - .../elasticsearch/plugins/PluginManager.java | 686 ----------------- .../plugins/PluginManagerCliParser.java | 256 ------- .../elasticsearch/plugins/PluginSecurity.java | 21 +- .../plugins/RemovePluginCommand.java | 77 ++ .../elasticsearch/plugins/plugin-install.help | 13 +- .../common/io/FileSystemUtilsTests.java | 85 -- ...nagerCliTests.java => PluginCliTests.java} | 15 +- .../loading/classpath/InClassPathPlugin.java | 35 - .../classpath/es-plugin-test.properties | 19 - distribution/src/main/resources/bin/plugin | 2 +- .../src/main/resources/bin/plugin.bat | Bin 1254 -> 1241 bytes .../plugins/InstallPluginCommandTests.java | 442 +++++++++++ .../plugins/ListPluginsCommandTests.java | 90 +++ .../plugins/PluginManagerPermissionTests.java | 377 --------- .../plugins/PluginManagerTests.java | 725 ------------------ .../plugins/PluginManagerUnitTests.java | 135 ---- .../plugins/RemovePluginCommandTests.java | 111 +++ .../common/cli/CliToolTestCase.java | 26 +- 27 files changed, 1327 insertions(+), 3063 deletions(-) delete mode 100644 core/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java create mode 100644 core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java create mode 100644 core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java create mode 100644 core/src/main/java/org/elasticsearch/plugins/PluginCli.java delete mode 100644 core/src/main/java/org/elasticsearch/plugins/PluginManager.java delete mode 100644 core/src/main/java/org/elasticsearch/plugins/PluginManagerCliParser.java create mode 100644 core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java rename core/src/test/java/org/elasticsearch/plugins/{PluginManagerCliTests.java => PluginCliTests.java} (78%) delete mode 100644 core/src/test/java/org/elasticsearch/plugins/loading/classpath/InClassPathPlugin.java delete mode 100644 core/src/test/resources/org/elasticsearch/plugins/loading/classpath/es-plugin-test.properties create mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java create mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java delete mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerPermissionTests.java delete mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java delete mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerUnitTests.java create mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java diff --git a/core/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java b/core/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java index 4f694e9af38..5a463258eb1 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java +++ b/core/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java @@ -50,7 +50,7 @@ public class HelpPrinter { } }); } catch (IOException ioe) { - ioe.printStackTrace(terminal.writer()); + throw new RuntimeException(ioe); } terminal.println(); } diff --git a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java index 82898b3e457..5e4bc09ad9f 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java +++ b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java @@ -132,8 +132,6 @@ public abstract class Terminal { protected abstract void doPrint(String msg, Object... args); - public abstract PrintWriter writer(); - private static class ConsoleTerminal extends Terminal { final Console console = System.console(); @@ -158,11 +156,6 @@ public abstract class Terminal { return console.readPassword(text, args); } - @Override - public PrintWriter writer() { - return console.writer(); - } - @Override public void printStackTrace(Throwable t) { t.printStackTrace(console.writer()); @@ -199,10 +192,5 @@ public abstract class Terminal { public void printStackTrace(Throwable t) { t.printStackTrace(printWriter); } - - @Override - public PrintWriter writer() { - return printWriter; - } } } diff --git a/core/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java b/core/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java deleted file mode 100644 index b99ef895430..00000000000 --- a/core/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.common.http.client; - -import org.apache.lucene.util.IOUtils; -import org.elasticsearch.Build; -import org.elasticsearch.ElasticsearchCorruptionException; -import org.elasticsearch.ElasticsearchTimeoutException; -import org.elasticsearch.Version; -import org.elasticsearch.common.Base64; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.common.unit.TimeValue; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; -import java.util.List; - -/** - * - */ -public class HttpDownloadHelper { - - private boolean useTimestamp = false; - private boolean skipExisting = false; - - public boolean download(URL source, Path dest, @Nullable DownloadProgress progress, TimeValue timeout) throws Exception { - if (Files.exists(dest) && skipExisting) { - return true; - } - - //don't do any progress, unless asked - if (progress == null) { - progress = new NullProgress(); - } - - //set the timestamp to the file date. - long timestamp = 0; - - boolean hasTimestamp = false; - if (useTimestamp && Files.exists(dest) ) { - timestamp = Files.getLastModifiedTime(dest).toMillis(); - hasTimestamp = true; - } - - GetThread getThread = new GetThread(source, dest, hasTimestamp, timestamp, progress); - - try { - getThread.setDaemon(true); - getThread.start(); - getThread.join(timeout.millis()); - - if (getThread.isAlive()) { - throw new ElasticsearchTimeoutException("The GET operation took longer than " + timeout + ", stopping it."); - } - } - catch (InterruptedException ie) { - return false; - } finally { - getThread.closeStreams(); - } - - return getThread.wasSuccessful(); - } - - public interface Checksummer { - /** Return the hex string for the given byte array */ - String checksum(byte[] filebytes); - /** Human-readable name for the checksum format */ - String name(); - } - - /** Checksummer for SHA1 */ - public static Checksummer SHA1_CHECKSUM = new Checksummer() { - @Override - public String checksum(byte[] filebytes) { - return MessageDigests.toHexString(MessageDigests.sha1().digest(filebytes)); - } - - @Override - public String name() { - return "SHA1"; - } - }; - - /** Checksummer for MD5 */ - public static Checksummer MD5_CHECKSUM = new Checksummer() { - @Override - public String checksum(byte[] filebytes) { - return MessageDigests.toHexString(MessageDigests.md5().digest(filebytes)); - } - - @Override - public String name() { - return "MD5"; - } - }; - - /** - * Download the given checksum URL to the destination and check the checksum - * @param checksumURL URL for the checksum file - * @param originalFile original file to calculate checksum of - * @param checksumFile destination to download the checksum file to - * @param hashFunc class used to calculate the checksum of the file - * @return true if the checksum was validated, false if it did not exist - * @throws Exception if the checksum failed to match - */ - public boolean downloadAndVerifyChecksum(URL checksumURL, Path originalFile, Path checksumFile, - @Nullable DownloadProgress progress, - TimeValue timeout, Checksummer hashFunc) throws Exception { - try { - if (download(checksumURL, checksumFile, progress, timeout)) { - byte[] fileBytes = Files.readAllBytes(originalFile); - List checksumLines = Files.readAllLines(checksumFile, StandardCharsets.UTF_8); - if (checksumLines.size() != 1) { - throw new ElasticsearchCorruptionException("invalid format for checksum file (" + - hashFunc.name() + "), expected 1 line, got: " + checksumLines.size()); - } - String checksumHex = checksumLines.get(0); - String fileHex = hashFunc.checksum(fileBytes); - if (fileHex.equals(checksumHex) == false) { - throw new ElasticsearchCorruptionException("incorrect hash (" + hashFunc.name() + - "), file hash: [" + fileHex + "], expected: [" + checksumHex + "]"); - } - return true; - } - } catch (FileNotFoundException | NoSuchFileException e) { - // checksum file doesn't exist - return false; - } finally { - IOUtils.deleteFilesIgnoringExceptions(checksumFile); - } - return false; - } - - /** - * Interface implemented for reporting - * progress of downloading. - */ - public interface DownloadProgress { - /** - * begin a download - */ - void beginDownload(); - - /** - * tick handler - */ - void onTick(); - - /** - * end a download - */ - void endDownload(); - } - - /** - * do nothing with progress info - */ - public static class NullProgress implements DownloadProgress { - - /** - * begin a download - */ - @Override - public void beginDownload() { - - } - - /** - * tick handler - */ - @Override - public void onTick() { - } - - /** - * end a download - */ - @Override - public void endDownload() { - - } - } - - /** - * verbose progress system prints to some output stream - */ - public static class VerboseProgress implements DownloadProgress { - private int dots = 0; - // CheckStyle:VisibilityModifier OFF - bc - PrintWriter writer; - // CheckStyle:VisibilityModifier ON - - /** - * Construct a verbose progress reporter. - * - * @param writer the output stream. - */ - public VerboseProgress(PrintWriter writer) { - this.writer = writer; - } - - /** - * begin a download - */ - @Override - public void beginDownload() { - writer.print("Downloading "); - dots = 0; - } - - /** - * tick handler - */ - @Override - public void onTick() { - writer.print("."); - if (dots++ > 50) { - writer.flush(); - dots = 0; - } - } - - /** - * end a download - */ - @Override - public void endDownload() { - writer.println("DONE"); - writer.flush(); - } - } - - private class GetThread extends Thread { - - private final URL source; - private final Path dest; - private final boolean hasTimestamp; - private final long timestamp; - private final DownloadProgress progress; - - private boolean success = false; - private IOException ioexception = null; - private InputStream is = null; - private OutputStream os = null; - private URLConnection connection; - private int redirections = 0; - - GetThread(URL source, Path dest, boolean h, long t, DownloadProgress p) { - this.source = source; - this.dest = dest; - hasTimestamp = h; - timestamp = t; - progress = p; - } - - @Override - public void run() { - try { - success = get(); - } catch (IOException ioex) { - ioexception = ioex; - } - } - - private boolean get() throws IOException { - - connection = openConnection(source); - - if (connection == null) { - return false; - } - - boolean downloadSucceeded = downloadFile(); - - //if (and only if) the use file time option is set, then - //the saved file now has its timestamp set to that of the - //downloaded file - if (downloadSucceeded && useTimestamp) { - updateTimeStamp(); - } - - return downloadSucceeded; - } - - - private boolean redirectionAllowed(URL aSource, URL aDest) throws IOException { - // Argh, github does this... -// if (!(aSource.getProtocol().equals(aDest.getProtocol()) || ("http" -// .equals(aSource.getProtocol()) && "https".equals(aDest -// .getProtocol())))) { -// String message = "Redirection detected from " -// + aSource.getProtocol() + " to " + aDest.getProtocol() -// + ". Protocol switch unsafe, not allowed."; -// throw new IOException(message); -// } - - redirections++; - if (redirections > 5) { - String message = "More than " + 5 + " times redirected, giving up"; - throw new IOException(message); - } - - - return true; - } - - private URLConnection openConnection(URL aSource) throws IOException { - - // set up the URL connection - URLConnection connection = aSource.openConnection(); - // modify the headers - // NB: things like user authentication could go in here too. - if (hasTimestamp) { - connection.setIfModifiedSince(timestamp); - } - - // in case the plugin manager is its own project, this can become an authenticator - boolean isSecureProcotol = "https".equalsIgnoreCase(aSource.getProtocol()); - boolean isAuthInfoSet = !Strings.isNullOrEmpty(aSource.getUserInfo()); - if (isAuthInfoSet) { - if (!isSecureProcotol) { - throw new IOException("Basic auth is only supported for HTTPS!"); - } - String basicAuth = Base64.encodeBytes(aSource.getUserInfo().getBytes(StandardCharsets.UTF_8)); - connection.setRequestProperty("Authorization", "Basic " + basicAuth); - } - - if (connection instanceof HttpURLConnection) { - ((HttpURLConnection) connection).setInstanceFollowRedirects(false); - connection.setUseCaches(true); - connection.setConnectTimeout(5000); - } - connection.setRequestProperty("ES-Version", Version.CURRENT.toString()); - connection.setRequestProperty("ES-Build-Hash", Build.CURRENT.shortHash()); - connection.setRequestProperty("User-Agent", "elasticsearch-plugin-manager"); - - // connect to the remote site (may take some time) - connection.connect(); - - // First check on a 301 / 302 (moved) response (HTTP only) - if (connection instanceof HttpURLConnection) { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - int responseCode = httpConnection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || - responseCode == HttpURLConnection.HTTP_MOVED_TEMP || - responseCode == HttpURLConnection.HTTP_SEE_OTHER) { - String newLocation = httpConnection.getHeaderField("Location"); - URL newURL = new URL(newLocation); - if (!redirectionAllowed(aSource, newURL)) { - return null; - } - return openConnection(newURL); - } - // next test for a 304 result (HTTP only) - long lastModified = httpConnection.getLastModified(); - if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED - || (lastModified != 0 && hasTimestamp && timestamp >= lastModified)) { - // not modified so no file download. just return - // instead and trace out something so the user - // doesn't think that the download happened when it - // didn't - return null; - } - // test for 401 result (HTTP only) - if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - String message = "HTTP Authorization failure"; - throw new IOException(message); - } - } - - //REVISIT: at this point even non HTTP connections may - //support the if-modified-since behaviour -we just check - //the date of the content and skip the write if it is not - //newer. Some protocols (FTP) don't include dates, of - //course. - return connection; - } - - private boolean downloadFile() throws FileNotFoundException, IOException { - IOException lastEx = null; - for (int i = 0; i < 3; i++) { - // this three attempt trick is to get round quirks in different - // Java implementations. Some of them take a few goes to bind - // property; we ignore the first couple of such failures. - try { - is = connection.getInputStream(); - break; - } catch (IOException ex) { - lastEx = ex; - } - } - if (is == null) { - throw lastEx; - } - - os = Files.newOutputStream(dest); - progress.beginDownload(); - boolean finished = false; - try { - byte[] buffer = new byte[1024 * 100]; - int length; - while (!isInterrupted() && (length = is.read(buffer)) >= 0) { - os.write(buffer, 0, length); - progress.onTick(); - } - finished = !isInterrupted(); - } finally { - if (!finished) { - // we have started to (over)write dest, but failed. - // Try to delete the garbage we'd otherwise leave - // behind. - IOUtils.closeWhileHandlingException(os, is); - IOUtils.deleteFilesIgnoringExceptions(dest); - } else { - IOUtils.close(os, is); - } - } - progress.endDownload(); - return true; - } - - private void updateTimeStamp() throws IOException { - long remoteTimestamp = connection.getLastModified(); - if (remoteTimestamp != 0) { - Files.setLastModifiedTime(dest, FileTime.fromMillis(remoteTimestamp)); - } - } - - /** - * Has the download completed successfully? - *

- * Re-throws any exception caught during executaion.

- */ - boolean wasSuccessful() throws IOException { - if (ioexception != null) { - throw ioexception; - } - return success; - } - - /** - * Closes streams, interrupts the download, may delete the - * output file. - */ - void closeStreams() throws IOException { - interrupt(); - if (success) { - IOUtils.close(is, os); - } else { - IOUtils.closeWhileHandlingException(is, os); - if (dest != null && Files.exists(dest)) { - IOUtils.deleteFilesIgnoringExceptions(dest); - } - } - } - } -} diff --git a/core/src/main/java/org/elasticsearch/common/io/FileSystemUtils.java b/core/src/main/java/org/elasticsearch/common/io/FileSystemUtils.java index 08761f84ff5..ab65f090364 100644 --- a/core/src/main/java/org/elasticsearch/common/io/FileSystemUtils.java +++ b/core/src/main/java/org/elasticsearch/common/io/FileSystemUtils.java @@ -52,33 +52,6 @@ public final class FileSystemUtils { private FileSystemUtils() {} // only static methods - /** - * Returns true iff a file under the given root has one of the given extensions. This method - * will travers directories recursively and will terminate once any of the extensions was found. This - * methods will not follow any links. - * - * @param root the root directory to travers. Must be a directory - * @param extensions the file extensions to look for - * @return true iff a file under the given root has one of the given extensions, otherwise false - * @throws IOException if an IOException occurs or if the given root path is not a directory. - */ - public static boolean hasExtensions(Path root, final String... extensions) throws IOException { - final AtomicBoolean retVal = new AtomicBoolean(false); - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - for (String extension : extensions) { - if (file.getFileName().toString().endsWith(extension)) { - retVal.set(true); - return FileVisitResult.TERMINATE; - } - } - return super.visitFile(file, attrs); - } - }); - return retVal.get(); - } - /** * Returns true iff one of the files exists otherwise false */ @@ -168,167 +141,6 @@ public final class FileSystemUtils { return new BufferedReader(reader); } - /** - * This utility copy a full directory content (excluded) under - * a new directory but without overwriting existing files. - * - * When a file already exists in destination dir, the source file is copied under - * destination directory but with a suffix appended if set or source file is ignored - * if suffix is not set (null). - * @param source Source directory (for example /tmp/es/src) - * @param destination Destination directory (destination directory /tmp/es/dst) - * @param suffix When not null, files are copied with a suffix appended to the original name (eg: ".new") - * When null, files are ignored - */ - public static void moveFilesWithoutOverwriting(Path source, final Path destination, final String suffix) throws IOException { - - // Create destination dir - Files.createDirectories(destination); - - final int configPathRootLevel = source.getNameCount(); - - // We walk through the file tree from - Files.walkFileTree(source, new SimpleFileVisitor() { - private Path buildPath(Path path) { - return destination.resolve(path); - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - // We are now in dir. We need to remove root of config files to have a relative path - - // If we are not walking in root dir, we might be able to copy its content - // if it does not already exist - if (configPathRootLevel != dir.getNameCount()) { - Path subpath = dir.subpath(configPathRootLevel, dir.getNameCount()); - Path path = buildPath(subpath); - if (!Files.exists(path)) { - // We just move the structure to new dir - // we can't do atomic move here since src / dest might be on different mounts? - move(dir, path); - // We just ignore sub files from here - return FileVisitResult.SKIP_SUBTREE; - } - } - - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Path subpath = null; - - if (configPathRootLevel != file.getNameCount()) { - subpath = file.subpath(configPathRootLevel, file.getNameCount()); - } - Path path = buildPath(subpath); - - if (!Files.exists(path)) { - // We just move the new file to new dir - move(file, path); - } else if (suffix != null) { - if (!isSameFile(file, path)) { - // If it already exists we try to copy this new version appending suffix to its name - path = path.resolveSibling(path.getFileName().toString().concat(suffix)); - // We just move the file to new dir but with a new name (appended with suffix) - Files.move(file, path, StandardCopyOption.REPLACE_EXISTING); - } - } - - return FileVisitResult.CONTINUE; - } - - /** - * Compares the content of two paths by comparing them - */ - private boolean isSameFile(Path first, Path second) throws IOException { - // do quick file size comparison before hashing - boolean sameFileSize = Files.size(first) == Files.size(second); - if (!sameFileSize) { - return false; - } - - byte[] firstBytes = Files.readAllBytes(first); - byte[] secondBytes = Files.readAllBytes(second); - return Arrays.equals(firstBytes, secondBytes); - } - }); - } - - /** - * Copy recursively a dir to a new location - * @param source source dir - * @param destination destination dir - */ - public static void copyDirectoryRecursively(Path source, Path destination) throws IOException { - Files.walkFileTree(source, new TreeCopier(source, destination, false)); - } - - /** - * Move or rename a file to a target file. This method supports moving a file from - * different filesystems (not supported by Files.move()). - * - * @param source source file - * @param destination destination file - */ - public static void move(Path source, Path destination) throws IOException { - try { - // We can't use atomic move here since source & target can be on different filesystems. - Files.move(source, destination); - } catch (DirectoryNotEmptyException e) { - Files.walkFileTree(source, new TreeCopier(source, destination, true)); - } - } - - // TODO: note that this will fail if source and target are on different NIO.2 filesystems. - - static class TreeCopier extends SimpleFileVisitor { - private final Path source; - private final Path target; - private final boolean delete; - - TreeCopier(Path source, Path target, boolean delete) { - this.source = source; - this.target = target; - this.delete = delete; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - Path newDir = target.resolve(source.relativize(dir)); - try { - Files.copy(dir, newDir); - } catch (FileAlreadyExistsException x) { - // We ignore this - } catch (IOException x) { - return SKIP_SUBTREE; - } - return CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - if (delete) { - IOUtils.rm(dir); - } - return CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Path newFile = target.resolve(source.relativize(file)); - try { - Files.copy(file, newFile); - if (delete) { - Files.deleteIfExists(file); - } - } catch (IOException x) { - // We ignore this - } - return CONTINUE; - } - } - /** * Returns an array of all files in the given directory matching. */ diff --git a/core/src/main/java/org/elasticsearch/common/logging/log4j/TerminalAppender.java b/core/src/main/java/org/elasticsearch/common/logging/log4j/TerminalAppender.java index 3c60c44d3e3..6e626060542 100644 --- a/core/src/main/java/org/elasticsearch/common/logging/log4j/TerminalAppender.java +++ b/core/src/main/java/org/elasticsearch/common/logging/log4j/TerminalAppender.java @@ -25,7 +25,7 @@ import org.apache.log4j.spi.LoggingEvent; import org.elasticsearch.common.cli.Terminal; /** - * TerminalAppender logs event to Terminal.DEFAULT. It is used for example by the PluginManagerCliParser. + * TerminalAppender logs event to Terminal.DEFAULT. It is used for example by the PluginCli. * */ public class TerminalAppender extends AppenderSkeleton { @Override diff --git a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java new file mode 100644 index 00000000000..5ea15f60d49 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -0,0 +1,402 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.bootstrap.JarHell; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.io.FileSystemUtils; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +import static java.util.Collections.unmodifiableSet; +import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE; +import static org.elasticsearch.common.util.set.Sets.newHashSet; + +/** + * A command for the plugin cli to install a plugin into elasticsearch. + * + * The install command takes a plugin id, which may be any of the following: + *
    + *
  • An official elasticsearch plugin name
  • + *
  • Maven coordinates to a plugin zip
  • + *
  • A URL to a plugin zip
  • + *
+ * + * Plugins are packaged as zip files. Each packaged plugin must contain a + * plugin properties file. See {@link PluginInfo}. + *

+ * The installation process first extracts the plugin files into a temporary + * directory in order to verify the plugin satisfies the following requirements: + *

    + *
  • Jar hell does not exist, either between the plugin's own jars, or with elasticsearch
  • + *
  • The plugin is not a module already provided with elasticsearch
  • + *
  • If the plugin contains extra security permissions, the policy file is validated
  • + *
+ *

+ * A plugin may also contain an optional {@code bin} directory which contains scripts. The + * scripts will be installed into a subdirectory of the elasticsearch bin directory, using + * the name of the plugin, and the scripts will be marked executable. + *

+ * A plugin may also contain an optional {@code config} directory which contains configuration + * files specific to the plugin. The config files be installed into a subdirectory of the + * elasticsearch config directory, using the name of the plugin. If any files to be installed + * already exist, they will be skipped. + */ +class InstallPluginCommand extends CliTool.Command { + + private static final String PROPERTY_SUPPORT_STAGING_URLS = "es.plugins.staging"; + + // TODO: make this a resource file generated by gradle + static final Set MODULES = unmodifiableSet(newHashSet( + "lang-expression", + "lang-groovy")); + + // TODO: make this a resource file generated by gradle + static final Set OFFICIAL_PLUGINS = unmodifiableSet(newHashSet( + "analysis-icu", + "analysis-kuromoji", + "analysis-phonetic", + "analysis-smartcn", + "analysis-stempel", + "delete-by-query", + "discovery-azure", + "discovery-ec2", + "discovery-gce", + "discovery-multicast", + "lang-javascript", + "lang-painless", + "lang-python", + "mapper-attachments", + "mapper-murmur3", + "mapper-size", + "repository-azure", + "repository-hdfs", + "repository-s3", + "store-smb")); + + private final String pluginId; + private final boolean batch; + + InstallPluginCommand(Terminal terminal, String pluginId, boolean batch) { + super(terminal); + this.pluginId = pluginId; + this.batch = batch; + } + + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + + // TODO: remove this leniency!! is it needed anymore? + if (Files.exists(env.pluginsFile()) == false) { + terminal.println("Plugins directory [%s] does not exist. Creating...", env.pluginsFile()); + Files.createDirectory(env.pluginsFile()); + } + + if (Environment.isWritable(env.pluginsFile()) == false) { + throw new IOException("Plugins directory is read only: " + env.pluginsFile()); + } + + Path pluginZip = download(pluginId, env.tmpFile()); + Path extractedZip = unzip(pluginZip, env.pluginsFile()); + install(extractedZip, env); + + return CliTool.ExitStatus.OK; + } + + /** Downloads the plugin and returns the file it was downloaded to. */ + private Path download(String pluginId, Path tmpDir) throws IOException { + if (OFFICIAL_PLUGINS.contains(pluginId)) { + final String version = Version.CURRENT.toString(); + final String url; + if (System.getProperty(PROPERTY_SUPPORT_STAGING_URLS, "false").equals("true")) { + url = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%1$s-%2$s/org/elasticsearch/plugin/%3$s/%1$s/%3$s-%1$s.zip", + version, Build.CURRENT.shortHash(), pluginId); + } else { + url = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%1$s/%2$s/%1$s-%2$s.zip", + pluginId, version); + } + terminal.println("-> Downloading " + pluginId + " from elastic"); + return downloadZipAndChecksum(url, tmpDir); + } + + // now try as maven coordinates, a valid URL would only have a single colon + String[] coordinates = pluginId.split(":"); + if (coordinates.length == 3) { + String mavenUrl = String.format(Locale.ROOT, "https://repo1.maven.org/maven2/%1$s/%2$s/%3$s/%2$s-%3$s.zip", + coordinates[0].replace(".", "/") /* groupId */, coordinates[1] /* artifactId */, coordinates[2] /* version */); + terminal.println("-> Downloading " + pluginId + " from maven central"); + return downloadZipAndChecksum(mavenUrl, tmpDir); + } + + // fall back to plain old URL + terminal.println("-> Downloading " + URLDecoder.decode(pluginId, "UTF-8")); + return downloadZip(pluginId, tmpDir); + } + + /** Downloads a zip from the url, into a temp file under the given temp dir. */ + private Path downloadZip(String urlString, Path tmpDir) throws IOException { + URL url = new URL(urlString); + Path zip = Files.createTempFile(tmpDir, null, ".zip"); + try (InputStream in = url.openStream()) { + // must overwrite since creating the temp file above actually created the file + Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING); + } + return zip; + } + + /** Downloads a zip from the url, as well as a SHA1 checksum, and checks the checksum. */ + private Path downloadZipAndChecksum(String urlString, Path tmpDir) throws IOException { + Path zip = downloadZip(urlString, tmpDir); + + URL checksumUrl = new URL(urlString + ".sha1"); + final String expectedChecksum; + try (InputStream in = checksumUrl.openStream()) { + BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + expectedChecksum = checksumReader.readLine(); + if (checksumReader.readLine() != null) { + throw new IllegalArgumentException("Invalid checksum file at " + urlString.toString()); + } + } + + byte[] zipbytes = Files.readAllBytes(zip); + String gotChecksum = MessageDigests.toHexString(MessageDigests.sha1().digest(zipbytes)); + if (expectedChecksum.equals(gotChecksum) == false) { + throw new IllegalStateException("SHA1 mismatch, expected " + expectedChecksum + " but got " + gotChecksum); + } + + return zip; + } + + private Path unzip(Path zip, Path pluginsDir) throws IOException { + // unzip plugin to a staging temp dir + Path target = Files.createTempDirectory(pluginsDir, ".installing-"); + Files.createDirectories(target); + + // TODO: we should wrap this in a try/catch and try deleting the target dir on failure? + try (ZipInputStream zipInput = new ZipInputStream(Files.newInputStream(zip))) { + ZipEntry entry; + byte[] buffer = new byte[8192]; + while ((entry = zipInput.getNextEntry()) != null) { + Path targetFile = target.resolve(entry.getName()); + // TODO: handle name being an absolute path + + // be on the safe side: do not rely on that directories are always extracted + // before their children (although this makes sense, but is it guaranteed?) + Files.createDirectories(targetFile.getParent()); + if (entry.isDirectory() == false) { + try (OutputStream out = Files.newOutputStream(targetFile)) { + int len; + while((len = zipInput.read(buffer)) >= 0) { + out.write(buffer, 0, len); + } + } + } + zipInput.closeEntry(); + } + } + return target; + } + + /** Load information about the plugin, and verify it can be installed with no errors. */ + private PluginInfo verify(Path pluginRoot, Environment env) throws Exception { + // read and validate the plugin descriptor + PluginInfo info = PluginInfo.readFromProperties(pluginRoot); + terminal.println(VERBOSE, "%s", info); + + // don't let luser install plugin as a module... + // they might be unavoidably in maven central and are packaged up the same way) + if (MODULES.contains(info.getName())) { + throw new IOException("plugin '" + info.getName() + "' cannot be installed like this, it is a system module"); + } + + // check for jar hell before any copying + jarHellCheck(pluginRoot, env.pluginsFile(), info.isIsolated()); + + // read optional security policy (extra permissions) + // if it exists, confirm or warn the user + Path policy = pluginRoot.resolve(PluginInfo.ES_PLUGIN_POLICY); + if (Files.exists(policy)) { + PluginSecurity.readPolicy(policy, terminal, env, batch); + } + + return info; + } + + /** check a candidate plugin for jar hell before installing it */ + private void jarHellCheck(Path candidate, Path pluginsDir, boolean isolated) throws Exception { + // create list of current jars in classpath + final List jars = new ArrayList<>(); + jars.addAll(Arrays.asList(JarHell.parseClassPath())); + + // read existing bundles. this does some checks on the installation too. + List bundles = PluginsService.getPluginBundles(pluginsDir); + + // if we aren't isolated, we need to jarhellcheck against any other non-isolated plugins + // thats always the first bundle + if (isolated == false) { + jars.addAll(bundles.get(0).urls); + } + + // add plugin jars to the list + Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar"); + for (Path jar : pluginJars) { + jars.add(jar.toUri().toURL()); + } + // TODO: no jars should be an error + // TODO: verify the classname exists in one of the jars! + + // check combined (current classpath + new jars to-be-added) + JarHell.checkJarHell(jars.toArray(new URL[jars.size()])); + } + + /** + * Installs the plugin from {@code tmpRoot} into the plugins dir. + * If the plugin has a bin dir and/or a config dir, those are copied. + */ + private void install(Path tmpRoot, Environment env) throws Exception { + List deleteOnFailure = new ArrayList<>(); + deleteOnFailure.add(tmpRoot); + + try { + PluginInfo info = verify(tmpRoot, env); + + final Path destination = env.pluginsFile().resolve(info.getName()); + if (Files.exists(destination)) { + throw new IOException("plugin directory " + destination.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + info.getName() + "' command"); + } + + Path tmpBinDir = tmpRoot.resolve("bin"); + if (Files.exists(tmpBinDir)) { + Path destBinDir = env.binFile().resolve(info.getName()); + deleteOnFailure.add(destBinDir); + installBin(info, tmpBinDir, destBinDir); + } + + Path tmpConfigDir = tmpRoot.resolve("config"); + if (Files.exists(tmpConfigDir)) { + // some files may already exist, and we don't remove plugin config files on plugin removal, + // so any installed config files are left on failure too + installConfig(info, tmpConfigDir, env.configFile().resolve(info.getName())); + } + + Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE); + terminal.println("-> Installed " + info.getName()); + + } catch (Exception installProblem) { + try { + IOUtils.rm(deleteOnFailure.toArray(new Path[0])); + } catch (IOException exceptionWhileRemovingFiles) { + installProblem.addSuppressed(exceptionWhileRemovingFiles); + } + throw installProblem; + } + } + + /** Copies the files from {@code tmpBinDir} into {@code destBinDir}, along with permissions from dest dirs parent. */ + private void installBin(PluginInfo info, Path tmpBinDir, Path destBinDir) throws IOException { + if (Files.isDirectory(tmpBinDir) == false) { + throw new IOException("bin in plugin " + info.getName() + " is not a directory"); + } + Files.createDirectory(destBinDir); + + // setup file attributes for the installed files to those of the parent dir + Set perms = new HashSet<>(); + PosixFileAttributeView binAttrs = Files.getFileAttributeView(destBinDir.getParent(), PosixFileAttributeView.class); + if (binAttrs != null) { + perms = new HashSet<>(binAttrs.readAttributes().permissions()); + // setting execute bits, since this just means "the file is executable", and actual execution requires read + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.GROUP_EXECUTE); + perms.add(PosixFilePermission.OTHERS_EXECUTE); + } + + try (DirectoryStream stream = Files.newDirectoryStream(tmpBinDir)) { + for (Path srcFile : stream) { + if (Files.isDirectory(srcFile)) { + throw new IOException("Directories not allowed in bin dir for plugin " + info.getName()); + } + + Path destFile = destBinDir.resolve(tmpBinDir.relativize(srcFile)); + Files.copy(srcFile, destFile); + + if (perms.isEmpty() == false) { + PosixFileAttributeView view = Files.getFileAttributeView(destFile, PosixFileAttributeView.class); + view.setPermissions(perms); + } + } + } + IOUtils.rm(tmpBinDir); // clean up what we just copied + } + + /** + * Copies the files from {@code tmpConfigDir} into {@code destConfigDir}. + * Any files existing in both the source and destination will be skipped. + */ + private void installConfig(PluginInfo info, Path tmpConfigDir, Path destConfigDir) throws IOException { + if (Files.isDirectory(tmpConfigDir) == false) { + throw new IOException("config in plugin " + info.getName() + " is not a directory"); + } + + // create the plugin's config dir "if necessary" + Files.createDirectories(destConfigDir); + + try (DirectoryStream stream = Files.newDirectoryStream(tmpConfigDir)) { + for (Path srcFile : stream) { + if (Files.isDirectory(srcFile)) { + throw new IOException("Directories not allowed in config dir for plugin " + info.getName()); + } + + Path destFile = destConfigDir.resolve(tmpConfigDir.relativize(srcFile)); + if (Files.exists(destFile) == false) { + Files.copy(srcFile, destFile); + } + } + } + IOUtils.rm(tmpConfigDir); // clean up what we just copied + } +} diff --git a/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java b/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java new file mode 100644 index 00000000000..6abed4e6bc2 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +/** + * A command for the plugin cli to list plugins installed in elasticsearch. + */ +class ListPluginsCommand extends CliTool.Command { + + ListPluginsCommand(Terminal terminal) { + super(terminal); + } + + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + if (Files.exists(env.pluginsFile()) == false) { + throw new IOException("Plugins directory missing: " + env.pluginsFile()); + } + + terminal.println(Terminal.Verbosity.VERBOSE, "Plugins directory: " + env.pluginsFile()); + try (DirectoryStream stream = Files.newDirectoryStream(env.pluginsFile())) { + for (Path plugin : stream) { + terminal.println(plugin.getFileName().toString()); + } + } + + return CliTool.ExitStatus.OK; + } +} diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginCli.java b/core/src/main/java/org/elasticsearch/plugins/PluginCli.java new file mode 100644 index 00000000000..c69c07b3d6b --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/PluginCli.java @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import org.apache.commons.cli.CommandLine; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.CliToolConfig; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.logging.log4j.LogConfigurator; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.node.internal.InternalSettingsPreparer; + +import java.util.Locale; + +import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd; +import static org.elasticsearch.common.cli.CliToolConfig.Builder.option; + +/** + * A cli tool for adding, removing and listing plugins for elasticsearch. + */ +public class PluginCli extends CliTool { + + // commands + private static final String LIST_CMD_NAME = "list"; + private static final String INSTALL_CMD_NAME = "install"; + private static final String REMOVE_CMD_NAME = "remove"; + + // usage config + private static final CliToolConfig.Cmd LIST_CMD = cmd(LIST_CMD_NAME, ListPluginsCommand.class).build(); + private static final CliToolConfig.Cmd INSTALL_CMD = cmd(INSTALL_CMD_NAME, InstallPluginCommand.class) + .options(option("b", "batch").required(false)) + .build(); + private static final CliToolConfig.Cmd REMOVE_CMD = cmd(REMOVE_CMD_NAME, RemovePluginCommand.class).build(); + + static final CliToolConfig CONFIG = CliToolConfig.config("plugin", PluginCli.class) + .cmds(LIST_CMD, INSTALL_CMD, REMOVE_CMD) + .build(); + + public static void main(String[] args) { + // initialize default for es.logger.level because we will not read the logging.yml + String loggerLevel = System.getProperty("es.logger.level", "INFO"); + // Set the appender for all potential log files to terminal so that other components that use the logger print out the + // same terminal. + // The reason for this is that the plugin cli cannot be configured with a file appender because when the plugin command is + // executed there is no way of knowing where the logfiles should be placed. For example, if elasticsearch + // is run as service then the logs should be at /var/log/elasticsearch but when started from the tar they should be at es.home/logs. + // Therefore we print to Terminal. + Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.builder() + .put("appender.terminal.type", "terminal") + .put("rootLogger", "${es.logger.level}, terminal") + .put("es.logger.level", loggerLevel) + .build(), Terminal.DEFAULT); + // configure but do not read the logging conf file + LogConfigurator.configure(env.settings(), false); + int status = new PluginCli(Terminal.DEFAULT).execute(args).status(); + exit(status); + } + + @SuppressForbidden(reason = "Allowed to exit explicitly from #main()") + private static void exit(int status) { + System.exit(status); + } + + PluginCli(Terminal terminal) { + super(CONFIG, terminal); + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + switch (cmdName.toLowerCase(Locale.ROOT)) { + case LIST_CMD_NAME: + return new ListPluginsCommand(terminal); + case INSTALL_CMD_NAME: + return parseInstallPluginCommand(cli); + case REMOVE_CMD_NAME: + return parseRemovePluginCommand(cli); + default: + assert false : "can't get here as cmd name is validated before this method is called"; + return exitCmd(ExitStatus.USAGE); + } + } + + private Command parseInstallPluginCommand(CommandLine cli) { + String[] args = cli.getArgs(); + if (args.length != 1) { + return exitCmd(ExitStatus.USAGE, terminal, "Must supply a single plugin id argument"); + } + + boolean batch = System.console() == null; + if (cli.hasOption("b")) { + batch = true; + } + + return new InstallPluginCommand(terminal, args[0], batch); + } + + private Command parseRemovePluginCommand(CommandLine cli) { + String[] args = cli.getArgs(); + if (args.length != 1) { + return exitCmd(ExitStatus.USAGE, terminal, "Must supply a single plugin name argument"); + } + + return new RemovePluginCommand(terminal, args[0]); + } +} diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java index 76af7833f06..73464d054dd 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java @@ -82,7 +82,6 @@ public class PluginInfo implements Streamable, ToXContent { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Property [name] is missing in [" + descriptor + "]"); } - PluginManager.checkForForbiddenName(name); String description = props.getProperty("description"); if (description == null) { throw new IllegalArgumentException("Property [description] is missing for plugin [" + name + "]"); diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginManager.java b/core/src/main/java/org/elasticsearch/plugins/PluginManager.java deleted file mode 100644 index a107c957bd4..00000000000 --- a/core/src/main/java/org/elasticsearch/plugins/PluginManager.java +++ /dev/null @@ -1,686 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugins; - -import org.apache.lucene.util.IOUtils; -import org.elasticsearch.Build; -import org.elasticsearch.ElasticsearchCorruptionException; -import org.elasticsearch.ElasticsearchTimeoutException; -import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.Version; -import org.elasticsearch.bootstrap.JarHell; -import org.elasticsearch.common.Randomness; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.cli.Terminal; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.http.client.HttpDownloadHelper; -import org.elasticsearch.common.io.FileSystemUtils; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.env.Environment; -import org.elasticsearch.plugins.PluginsService.Bundle; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.DirectoryStream; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.GroupPrincipal; -import java.nio.file.attribute.PosixFileAttributeView; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.UserPrincipal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.StreamSupport; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import static java.util.Collections.unmodifiableSet; -import static org.elasticsearch.common.Strings.hasLength; -import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE; -import static org.elasticsearch.common.io.FileSystemUtils.moveFilesWithoutOverwriting; -import static org.elasticsearch.common.util.set.Sets.newHashSet; - -/** - * - */ -public class PluginManager { - - public static final String PROPERTY_SUPPORT_STAGING_URLS = "es.plugins.staging"; - - public enum OutputMode { - DEFAULT, SILENT, VERBOSE - } - - private static final Set BLACKLIST = unmodifiableSet(newHashSet( - "elasticsearch", - "elasticsearch.bat", - "elasticsearch.in.sh", - "plugin", - "plugin.bat", - "service.bat")); - - static final Set MODULES = unmodifiableSet(newHashSet( - "lang-expression", - "lang-groovy")); - - static final Set OFFICIAL_PLUGINS = unmodifiableSet(newHashSet( - "analysis-icu", - "analysis-kuromoji", - "analysis-phonetic", - "analysis-smartcn", - "analysis-stempel", - "delete-by-query", - "discovery-azure", - "discovery-ec2", - "discovery-gce", - "discovery-multicast", - "ingest-geoip", - "lang-javascript", - "lang-painless", - "lang-python", - "mapper-attachments", - "mapper-murmur3", - "mapper-size", - "repository-azure", - "repository-hdfs", - "repository-s3", - "store-smb")); - - private final Environment environment; - private URL url; - private OutputMode outputMode; - private TimeValue timeout; - - public PluginManager(Environment environment, URL url, OutputMode outputMode, TimeValue timeout) { - this.environment = environment; - this.url = url; - this.outputMode = outputMode; - this.timeout = timeout; - } - - public void downloadAndExtract(String name, Terminal terminal, boolean batch) throws IOException { - if (name == null && url == null) { - throw new IllegalArgumentException("plugin name or url must be supplied with install."); - } - - if (!Files.exists(environment.pluginsFile())) { - terminal.println("Plugins directory [%s] does not exist. Creating...", environment.pluginsFile()); - Files.createDirectory(environment.pluginsFile()); - } - - if (!Environment.isWritable(environment.pluginsFile())) { - throw new IOException("plugin directory " + environment.pluginsFile() + " is read only"); - } - - PluginHandle pluginHandle; - if (name != null) { - pluginHandle = PluginHandle.parse(name); - checkForForbiddenName(pluginHandle.name); - } else { - // if we have no name but url, use temporary name that will be overwritten later - pluginHandle = new PluginHandle("temp_name" + Randomness.get().nextInt(), null, null); - } - - Path pluginFile = download(pluginHandle, terminal); - extract(pluginHandle, terminal, pluginFile, batch); - } - - private Path download(PluginHandle pluginHandle, Terminal terminal) throws IOException { - Path pluginFile = pluginHandle.newDistroFile(environment); - - HttpDownloadHelper downloadHelper = new HttpDownloadHelper(); - boolean downloaded = false; - boolean verified = false; - HttpDownloadHelper.DownloadProgress progress; - if (outputMode == OutputMode.SILENT) { - progress = new HttpDownloadHelper.NullProgress(); - } else { - progress = new HttpDownloadHelper.VerboseProgress(terminal.writer()); - } - - // first, try directly from the URL provided - if (url != null) { - URL pluginUrl = url; - boolean isSecureProcotol = "https".equalsIgnoreCase(pluginUrl.getProtocol()); - boolean isAuthInfoSet = !Strings.isNullOrEmpty(pluginUrl.getUserInfo()); - if (isAuthInfoSet && !isSecureProcotol) { - throw new IOException("Basic auth is only supported for HTTPS!"); - } - - terminal.println("Trying %s ...", pluginUrl.toExternalForm()); - try { - downloadHelper.download(pluginUrl, pluginFile, progress, this.timeout); - downloaded = true; - terminal.println("Verifying %s checksums if available ...", pluginUrl.toExternalForm()); - Tuple sha1Info = pluginHandle.newChecksumUrlAndFile(environment, pluginUrl, "sha1"); - verified = downloadHelper.downloadAndVerifyChecksum(sha1Info.v1(), pluginFile, - sha1Info.v2(), progress, this.timeout, HttpDownloadHelper.SHA1_CHECKSUM); - Tuple md5Info = pluginHandle.newChecksumUrlAndFile(environment, pluginUrl, "md5"); - verified = verified || downloadHelper.downloadAndVerifyChecksum(md5Info.v1(), pluginFile, - md5Info.v2(), progress, this.timeout, HttpDownloadHelper.MD5_CHECKSUM); - } catch (ElasticsearchTimeoutException | ElasticsearchCorruptionException e) { - throw e; - } catch (Exception e) { - // ignore - terminal.println("Failed: %s", ExceptionsHelper.detailedMessage(e)); - } - } else { - if (PluginHandle.isOfficialPlugin(pluginHandle.name, pluginHandle.user, pluginHandle.version)) { - checkForOfficialPlugins(pluginHandle.name); - } - } - - if (!downloaded && url == null) { - // We try all possible locations - for (URL url : pluginHandle.urls()) { - terminal.println("Trying %s ...", url.toExternalForm()); - try { - downloadHelper.download(url, pluginFile, progress, this.timeout); - downloaded = true; - terminal.println("Verifying %s checksums if available ...", url.toExternalForm()); - Tuple sha1Info = pluginHandle.newChecksumUrlAndFile(environment, url, "sha1"); - verified = downloadHelper.downloadAndVerifyChecksum(sha1Info.v1(), pluginFile, - sha1Info.v2(), progress, this.timeout, HttpDownloadHelper.SHA1_CHECKSUM); - Tuple md5Info = pluginHandle.newChecksumUrlAndFile(environment, url, "md5"); - verified = verified || downloadHelper.downloadAndVerifyChecksum(md5Info.v1(), pluginFile, - md5Info.v2(), progress, this.timeout, HttpDownloadHelper.MD5_CHECKSUM); - break; - } catch (ElasticsearchTimeoutException | ElasticsearchCorruptionException e) { - throw e; - } catch (Exception e) { - terminal.println(VERBOSE, "Failed: %s", ExceptionsHelper.detailedMessage(e)); - } - } - } - - if (!downloaded) { - // try to cleanup what we downloaded - IOUtils.deleteFilesIgnoringExceptions(pluginFile); - throw new IOException("failed to download out of all possible locations..., use --verbose to get detailed information"); - } - - if (verified == false) { - terminal.println("NOTE: Unable to verify checksum for downloaded plugin (unable to find .sha1 or .md5 file to verify)"); - } - return pluginFile; - } - - private void extract(PluginHandle pluginHandle, Terminal terminal, Path pluginFile, boolean batch) throws IOException { - // unzip plugin to a staging temp dir, named for the plugin - Path tmp = Files.createTempDirectory(environment.tmpFile(), null); - Path root = tmp.resolve(pluginHandle.name); - unzipPlugin(pluginFile, root); - - // find the actual root (in case its unzipped with extra directory wrapping) - root = findPluginRoot(root); - - // read and validate the plugin descriptor - PluginInfo info = PluginInfo.readFromProperties(root); - terminal.println(VERBOSE, "%s", info); - - // don't let luser install plugin as a module... - // they might be unavoidably in maven central and are packaged up the same way) - if (MODULES.contains(info.getName())) { - throw new IOException("plugin '" + info.getName() + "' cannot be installed like this, it is a system module"); - } - - // update name in handle based on 'name' property found in descriptor file - pluginHandle = new PluginHandle(info.getName(), pluginHandle.version, pluginHandle.user); - final Path extractLocation = pluginHandle.extractedDir(environment); - if (Files.exists(extractLocation)) { - throw new IOException("plugin directory " + extractLocation.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + pluginHandle.name + "' command"); - } - - // check for jar hell before any copying - jarHellCheck(root, info.isIsolated()); - - // read optional security policy (extra permissions) - // if it exists, confirm or warn the user - Path policy = root.resolve(PluginInfo.ES_PLUGIN_POLICY); - if (Files.exists(policy)) { - PluginSecurity.readPolicy(policy, terminal, environment, batch); - } - - // install plugin - FileSystemUtils.copyDirectoryRecursively(root, extractLocation); - terminal.println("Installed %s into %s", pluginHandle.name, extractLocation.toAbsolutePath()); - - // cleanup - tryToDeletePath(terminal, tmp, pluginFile); - - // take care of bin/ by moving and applying permissions if needed - Path sourcePluginBinDirectory = extractLocation.resolve("bin"); - Path destPluginBinDirectory = pluginHandle.binDir(environment); - boolean needToCopyBinDirectory = Files.exists(sourcePluginBinDirectory); - if (needToCopyBinDirectory) { - if (Files.exists(destPluginBinDirectory) && !Files.isDirectory(destPluginBinDirectory)) { - tryToDeletePath(terminal, extractLocation); - throw new IOException("plugin bin directory " + destPluginBinDirectory + " is not a directory"); - } - - try { - copyBinDirectory(sourcePluginBinDirectory, destPluginBinDirectory, pluginHandle.name, terminal); - } catch (IOException e) { - // rollback and remove potentially before installed leftovers - terminal.printError("Error copying bin directory [%s] to [%s], cleaning up, reason: %s", sourcePluginBinDirectory, destPluginBinDirectory, ExceptionsHelper.detailedMessage(e)); - tryToDeletePath(terminal, extractLocation, pluginHandle.binDir(environment)); - throw e; - } - - } - - Path sourceConfigDirectory = extractLocation.resolve("config"); - Path destConfigDirectory = pluginHandle.configDir(environment); - boolean needToCopyConfigDirectory = Files.exists(sourceConfigDirectory); - if (needToCopyConfigDirectory) { - if (Files.exists(destConfigDirectory) && !Files.isDirectory(destConfigDirectory)) { - tryToDeletePath(terminal, extractLocation, destPluginBinDirectory); - throw new IOException("plugin config directory " + destConfigDirectory + " is not a directory"); - } - - try { - terminal.println(VERBOSE, "Found config, moving to %s", destConfigDirectory.toAbsolutePath()); - moveFilesWithoutOverwriting(sourceConfigDirectory, destConfigDirectory, ".new"); - - if (Environment.getFileStore(destConfigDirectory).supportsFileAttributeView(PosixFileAttributeView.class)) { - //We copy owner, group and permissions from the parent ES_CONFIG directory, assuming they were properly set depending - // on how es was installed in the first place: can be root:elasticsearch (750) if es was installed from rpm/deb packages - // or most likely elasticsearch:elasticsearch if installed from tar/zip. As for permissions we don't rely on umask. - PosixFileAttributes parentDirAttributes = Files.getFileAttributeView(destConfigDirectory.getParent(), PosixFileAttributeView.class).readAttributes(); - //for files though, we make sure not to copy execute permissions from the parent dir and leave them untouched - Set baseFilePermissions = new HashSet<>(); - for (PosixFilePermission posixFilePermission : parentDirAttributes.permissions()) { - switch (posixFilePermission) { - case OWNER_EXECUTE: - case GROUP_EXECUTE: - case OTHERS_EXECUTE: - break; - default: - baseFilePermissions.add(posixFilePermission); - } - } - Files.walkFileTree(destConfigDirectory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (attrs.isRegularFile()) { - Set newFilePermissions = new HashSet<>(baseFilePermissions); - Set currentFilePermissions = Files.getPosixFilePermissions(file); - for (PosixFilePermission posixFilePermission : currentFilePermissions) { - switch (posixFilePermission) { - case OWNER_EXECUTE: - case GROUP_EXECUTE: - case OTHERS_EXECUTE: - newFilePermissions.add(posixFilePermission); - } - } - setPosixFileAttributes(file, parentDirAttributes.owner(), parentDirAttributes.group(), newFilePermissions); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - setPosixFileAttributes(dir, parentDirAttributes.owner(), parentDirAttributes.group(), parentDirAttributes.permissions()); - return FileVisitResult.CONTINUE; - } - }); - } else { - terminal.println(VERBOSE, "Skipping posix permissions - filestore doesn't support posix permission"); - } - - terminal.println(VERBOSE, "Installed %s into %s", pluginHandle.name, destConfigDirectory.toAbsolutePath()); - } catch (IOException e) { - terminal.printError("Error copying config directory [%s] to [%s], cleaning up, reason: %s", sourceConfigDirectory, destConfigDirectory, ExceptionsHelper.detailedMessage(e)); - tryToDeletePath(terminal, extractLocation, destPluginBinDirectory, destConfigDirectory); - throw e; - } - } - } - - private static void setPosixFileAttributes(Path path, UserPrincipal owner, GroupPrincipal group, Set permissions) throws IOException { - PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class); - fileAttributeView.setOwner(owner); - fileAttributeView.setGroup(group); - fileAttributeView.setPermissions(permissions); - } - - static void tryToDeletePath(Terminal terminal, Path ... paths) { - for (Path path : paths) { - try { - IOUtils.rm(path); - } catch (IOException e) { - terminal.printError(e); - } - } - } - - private void copyBinDirectory(Path sourcePluginBinDirectory, Path destPluginBinDirectory, String pluginName, Terminal terminal) throws IOException { - boolean canCopyFromSource = Files.exists(sourcePluginBinDirectory) && Files.isReadable(sourcePluginBinDirectory) && Files.isDirectory(sourcePluginBinDirectory); - if (canCopyFromSource) { - terminal.println(VERBOSE, "Found bin, moving to %s", destPluginBinDirectory.toAbsolutePath()); - if (Files.exists(destPluginBinDirectory)) { - IOUtils.rm(destPluginBinDirectory); - } - try { - Files.createDirectories(destPluginBinDirectory.getParent()); - FileSystemUtils.move(sourcePluginBinDirectory, destPluginBinDirectory); - } catch (IOException e) { - throw new IOException("Could not move [" + sourcePluginBinDirectory + "] to [" + destPluginBinDirectory + "]", e); - } - if (Environment.getFileStore(destPluginBinDirectory).supportsFileAttributeView(PosixFileAttributeView.class)) { - PosixFileAttributes parentDirAttributes = Files.getFileAttributeView(destPluginBinDirectory.getParent(), PosixFileAttributeView.class).readAttributes(); - //copy permissions from parent bin directory - Set filePermissions = new HashSet<>(); - for (PosixFilePermission posixFilePermission : parentDirAttributes.permissions()) { - switch (posixFilePermission) { - case OWNER_EXECUTE: - case GROUP_EXECUTE: - case OTHERS_EXECUTE: - break; - default: - filePermissions.add(posixFilePermission); - } - } - // add file execute permissions to existing perms, so execution will work. - filePermissions.add(PosixFilePermission.OWNER_EXECUTE); - filePermissions.add(PosixFilePermission.GROUP_EXECUTE); - filePermissions.add(PosixFilePermission.OTHERS_EXECUTE); - Files.walkFileTree(destPluginBinDirectory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (attrs.isRegularFile()) { - setPosixFileAttributes(file, parentDirAttributes.owner(), parentDirAttributes.group(), filePermissions); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - setPosixFileAttributes(dir, parentDirAttributes.owner(), parentDirAttributes.group(), parentDirAttributes.permissions()); - return FileVisitResult.CONTINUE; - } - }); - } else { - terminal.println(VERBOSE, "Skipping posix permissions - filestore doesn't support posix permission"); - } - terminal.println(VERBOSE, "Installed %s into %s", pluginName, destPluginBinDirectory.toAbsolutePath()); - } - } - - /** we check whether we need to remove the top-level folder while extracting - * sometimes (e.g. github) the downloaded archive contains a top-level folder which needs to be removed - */ - private Path findPluginRoot(Path dir) throws IOException { - if (Files.exists(dir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES))) { - return dir; - } else { - final Path[] topLevelFiles = FileSystemUtils.files(dir); - if (topLevelFiles.length == 1 && Files.isDirectory(topLevelFiles[0])) { - Path subdir = topLevelFiles[0]; - if (Files.exists(subdir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES))) { - return subdir; - } - } - } - throw new RuntimeException("Could not find plugin descriptor '" + PluginInfo.ES_PLUGIN_PROPERTIES + "' in plugin zip"); - } - - /** check a candidate plugin for jar hell before installing it */ - private void jarHellCheck(Path candidate, boolean isolated) throws IOException { - // create list of current jars in classpath - final List jars = new ArrayList<>(); - jars.addAll(Arrays.asList(JarHell.parseClassPath())); - - // read existing bundles. this does some checks on the installation too. - List bundles = PluginsService.getPluginBundles(environment.pluginsFile()); - - // if we aren't isolated, we need to jarhellcheck against any other non-isolated plugins - // thats always the first bundle - if (isolated == false) { - jars.addAll(bundles.get(0).urls); - } - - // add plugin jars to the list - Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar"); - for (Path jar : pluginJars) { - jars.add(jar.toUri().toURL()); - } - - // check combined (current classpath + new jars to-be-added) - try { - JarHell.checkJarHell(jars.toArray(new URL[jars.size()])); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private void unzipPlugin(Path zip, Path target) throws IOException { - Files.createDirectories(target); - - try (ZipInputStream zipInput = new ZipInputStream(Files.newInputStream(zip))) { - ZipEntry entry; - byte[] buffer = new byte[8192]; - while ((entry = zipInput.getNextEntry()) != null) { - Path targetFile = target.resolve(entry.getName()); - - // be on the safe side: do not rely on that directories are always extracted - // before their children (although this makes sense, but is it guaranteed?) - Files.createDirectories(targetFile.getParent()); - if (entry.isDirectory() == false) { - try (OutputStream out = Files.newOutputStream(targetFile)) { - int len; - while((len = zipInput.read(buffer)) >= 0) { - out.write(buffer, 0, len); - } - } - } - zipInput.closeEntry(); - } - } - } - - public void removePlugin(String name, Terminal terminal) throws IOException { - if (name == null) { - throw new IllegalArgumentException("plugin name must be supplied with remove [name]."); - } - PluginHandle pluginHandle = PluginHandle.parse(name); - boolean removed = false; - - checkForForbiddenName(pluginHandle.name); - Path pluginToDelete = pluginHandle.extractedDir(environment); - if (Files.exists(pluginToDelete)) { - terminal.println(VERBOSE, "Removing: %s", pluginToDelete); - try { - IOUtils.rm(pluginToDelete); - } catch (IOException ex){ - throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " + - pluginToDelete.toString(), ex); - } - removed = true; - } - Path binLocation = pluginHandle.binDir(environment); - if (Files.exists(binLocation)) { - terminal.println(VERBOSE, "Removing: %s", binLocation); - try { - IOUtils.rm(binLocation); - } catch (IOException ex){ - throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " + - binLocation.toString(), ex); - } - removed = true; - } - - if (removed) { - terminal.println("Removed %s", name); - } else { - terminal.println("Plugin %s not found. Run \"plugin list\" to get list of installed plugins.", name); - } - } - - static void checkForForbiddenName(String name) { - if (!hasLength(name) || BLACKLIST.contains(name.toLowerCase(Locale.ROOT))) { - throw new IllegalArgumentException("Illegal plugin name: " + name); - } - } - - protected static void checkForOfficialPlugins(String name) { - // We make sure that users can use only new short naming for official plugins only - if (!OFFICIAL_PLUGINS.contains(name)) { - throw new IllegalArgumentException(name + - " is not an official plugin so you should install it using elasticsearch/" + - name + "/latest naming form."); - } - } - - public Path[] getListInstalledPlugins() throws IOException { - if (!Files.exists(environment.pluginsFile())) { - return new Path[0]; - } - - try (DirectoryStream stream = Files.newDirectoryStream(environment.pluginsFile())) { - return StreamSupport.stream(stream.spliterator(), false).toArray(length -> new Path[length]); - } - } - - public void listInstalledPlugins(Terminal terminal) throws IOException { - Path[] plugins = getListInstalledPlugins(); - terminal.println("Installed plugins in %s:", environment.pluginsFile().toAbsolutePath()); - if (plugins == null || plugins.length == 0) { - terminal.println(" - No plugin detected"); - } else { - for (Path plugin : plugins) { - terminal.println(" - " + plugin.getFileName()); - } - } - } - - /** - * Helper class to extract properly user name, repository name, version and plugin name - * from plugin name given by a user. - */ - static class PluginHandle { - - final String version; - final String user; - final String name; - - PluginHandle(String name, String version, String user) { - this.version = version; - this.user = user; - this.name = name; - } - - List urls() { - List urls = new ArrayList<>(); - if (version != null) { - // Elasticsearch new download service uses groupId org.elasticsearch.plugin from 2.0.0 - if (user == null) { - if (!Strings.isNullOrEmpty(System.getProperty(PROPERTY_SUPPORT_STAGING_URLS))) { - addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip", version, Build.CURRENT.shortHash(), name, version, name, version)); - } - addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%s/%s/%s-%s.zip", name, version, name, version)); - } else { - // Elasticsearch old download service - addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/%1$s/%2$s/%2$s-%3$s.zip", user, name, version)); - // Maven central repository - addUrl(urls, String.format(Locale.ROOT, "https://search.maven.org/remotecontent?filepath=%1$s/%2$s/%3$s/%2$s-%3$s.zip", user.replace('.', '/'), name, version)); - // Sonatype repository - addUrl(urls, String.format(Locale.ROOT, "https://oss.sonatype.org/service/local/repositories/releases/content/%1$s/%2$s/%3$s/%2$s-%3$s.zip", user.replace('.', '/'), name, version)); - // Github repository - addUrl(urls, String.format(Locale.ROOT, "https://github.com/%1$s/%2$s/archive/%3$s.zip", user, name, version)); - } - } - if (user != null) { - // Github repository for master branch (assume site) - addUrl(urls, String.format(Locale.ROOT, "https://github.com/%1$s/%2$s/archive/master.zip", user, name)); - } - return urls; - } - - private static void addUrl(List urls, String url) { - try { - urls.add(new URL(url)); - } catch (MalformedURLException e) { - // We simply ignore malformed URL - } - } - - Path newDistroFile(Environment env) throws IOException { - return Files.createTempFile(env.tmpFile(), name, ".zip"); - } - - Tuple newChecksumUrlAndFile(Environment env, URL originalUrl, String suffix) throws IOException { - URL newUrl = new URL(originalUrl.toString() + "." + suffix); - return new Tuple<>(newUrl, Files.createTempFile(env.tmpFile(), name, ".zip." + suffix)); - } - - Path extractedDir(Environment env) { - return env.pluginsFile().resolve(name); - } - - Path binDir(Environment env) { - return env.binFile().resolve(name); - } - - Path configDir(Environment env) { - return env.configFile().resolve(name); - } - - static PluginHandle parse(String name) { - String[] elements = name.split("/"); - // We first consider the simplest form: pluginname - String repo = elements[0]; - String user = null; - String version = null; - - // We consider the form: username/pluginname - if (elements.length > 1) { - user = elements[0]; - repo = elements[1]; - - // We consider the form: username/pluginname/version - if (elements.length > 2) { - version = elements[2]; - } - } - - if (isOfficialPlugin(repo, user, version)) { - return new PluginHandle(repo, Version.CURRENT.number(), null); - } - - return new PluginHandle(repo, version, user); - } - - static boolean isOfficialPlugin(String repo, String user, String version) { - return version == null && user == null && !Strings.isNullOrEmpty(repo); - } - } - -} diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginManagerCliParser.java b/core/src/main/java/org/elasticsearch/plugins/PluginManagerCliParser.java deleted file mode 100644 index a8a51db971c..00000000000 --- a/core/src/main/java/org/elasticsearch/plugins/PluginManagerCliParser.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugins; - -import org.apache.commons.cli.CommandLine; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.cli.CliTool; -import org.elasticsearch.common.cli.CliToolConfig; -import org.elasticsearch.common.cli.Terminal; -import org.elasticsearch.common.logging.log4j.LogConfigurator; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.env.Environment; -import org.elasticsearch.node.internal.InternalSettingsPreparer; -import org.elasticsearch.plugins.PluginManager.OutputMode; - -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLDecoder; -import java.util.Locale; - -import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd; -import static org.elasticsearch.common.cli.CliToolConfig.Builder.option; - -public class PluginManagerCliParser extends CliTool { - - // By default timeout is 0 which means no timeout - public static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueMillis(0); - - private static final CliToolConfig CONFIG = CliToolConfig.config("plugin", PluginManagerCliParser.class) - .cmds(ListPlugins.CMD, Install.CMD, Remove.CMD) - .build(); - - public static void main(String[] args) { - // initialize default for es.logger.level because we will not read the logging.yml - String loggerLevel = System.getProperty("es.logger.level", "INFO"); - // Set the appender for all potential log files to terminal so that other components that use the logger print out the - // same terminal. - // The reason for this is that the plugin cli cannot be configured with a file appender because when the plugin command is - // executed there is no way of knowing where the logfiles should be placed. For example, if elasticsearch - // is run as service then the logs should be at /var/log/elasticsearch but when started from the tar they should be at es.home/logs. - // Therefore we print to Terminal. - Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.builder() - .put("appender.terminal.type", "terminal") - .put("rootLogger", "${es.logger.level}, terminal") - .put("es.logger.level", loggerLevel) - .build(), Terminal.DEFAULT); - // configure but do not read the logging conf file - LogConfigurator.configure(env.settings(), false); - int status = new PluginManagerCliParser().execute(args).status(); - exit(status); - } - - @SuppressForbidden(reason = "Allowed to exit explicitly from #main()") - private static void exit(int status) { - System.exit(status); - } - - public PluginManagerCliParser() { - super(CONFIG); - } - - public PluginManagerCliParser(Terminal terminal) { - super(CONFIG, terminal); - } - - @Override - protected Command parse(String cmdName, CommandLine cli) throws Exception { - switch (cmdName.toLowerCase(Locale.ROOT)) { - case Install.NAME: - return Install.parse(terminal, cli); - case ListPlugins.NAME: - return ListPlugins.parse(terminal, cli); - case Remove.NAME: - return Remove.parse(terminal, cli); - default: - assert false : "can't get here as cmd name is validated before this method is called"; - return exitCmd(ExitStatus.USAGE); - } - } - - /** - * List all installed plugins - */ - static class ListPlugins extends CliTool.Command { - - private static final String NAME = "list"; - - private static final CliToolConfig.Cmd CMD = cmd(NAME, ListPlugins.class).build(); - private final OutputMode outputMode; - - public static Command parse(Terminal terminal, CommandLine cli) { - OutputMode outputMode = OutputMode.DEFAULT; - if (cli.hasOption("s")) { - outputMode = OutputMode.SILENT; - } - if (cli.hasOption("v")) { - outputMode = OutputMode.VERBOSE; - } - - return new ListPlugins(terminal, outputMode); - } - - ListPlugins(Terminal terminal, OutputMode outputMode) { - super(terminal); - this.outputMode = outputMode; - } - - @Override - public ExitStatus execute(Settings settings, Environment env) throws Exception { - PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT); - pluginManager.listInstalledPlugins(terminal); - return ExitStatus.OK; - } - } - - /** - * Remove a plugin - */ - static class Remove extends CliTool.Command { - - private static final String NAME = "remove"; - - private static final CliToolConfig.Cmd CMD = cmd(NAME, Remove.class).build(); - - public static Command parse(Terminal terminal, CommandLine cli) { - String[] args = cli.getArgs(); - if (args.length == 0) { - return exitCmd(ExitStatus.USAGE, terminal, "plugin name is missing (type -h for help)"); - } - - OutputMode outputMode = OutputMode.DEFAULT; - if (cli.hasOption("s")) { - outputMode = OutputMode.SILENT; - } - if (cli.hasOption("v")) { - outputMode = OutputMode.VERBOSE; - } - - return new Remove(terminal, outputMode, args[0]); - } - - private OutputMode outputMode; - final String pluginName; - - Remove(Terminal terminal, OutputMode outputMode, String pluginToRemove) { - super(terminal); - this.outputMode = outputMode; - this.pluginName = pluginToRemove; - } - - @Override - public ExitStatus execute(Settings settings, Environment env) throws Exception { - - PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT); - terminal.println("-> Removing " + Strings.coalesceToEmpty(pluginName) + "..."); - pluginManager.removePlugin(pluginName, terminal); - return ExitStatus.OK; - } - } - - /** - * Installs a plugin - */ - static class Install extends Command { - - private static final String NAME = "install"; - - private static final CliToolConfig.Cmd CMD = cmd(NAME, Install.class) - .options(option("t", "timeout").required(false).hasArg(false)) - .options(option("b", "batch").required(false)) - .build(); - - static Command parse(Terminal terminal, CommandLine cli) { - String[] args = cli.getArgs(); - - // install [plugin-name/url] - if ((args == null) || (args.length == 0)) { - return exitCmd(ExitStatus.USAGE, terminal, "plugin name or url is missing (type -h for help)"); - } - String name = args[0]; - - URL optionalPluginUrl = null; - // try parsing cli argument as URL - try { - optionalPluginUrl = new URL(name); - name = null; - } catch (MalformedURLException e) { - // we tried to parse the cli argument as url and failed - // continue treating it as a symbolic plugin name like `analysis-icu` etc. - } - - TimeValue timeout = TimeValue.parseTimeValue(cli.getOptionValue("t"), DEFAULT_TIMEOUT, "cli"); - - OutputMode outputMode = OutputMode.DEFAULT; - if (cli.hasOption("s")) { - outputMode = OutputMode.SILENT; - } - if (cli.hasOption("v")) { - outputMode = OutputMode.VERBOSE; - } - - boolean batch = System.console() == null; - if (cli.hasOption("b")) { - batch = true; - } - - return new Install(terminal, name, outputMode, optionalPluginUrl, timeout, batch); - } - - final String name; - private OutputMode outputMode; - final URL url; - final TimeValue timeout; - final boolean batch; - - Install(Terminal terminal, String name, OutputMode outputMode, URL url, TimeValue timeout, boolean batch) { - super(terminal); - this.name = name; - this.outputMode = outputMode; - this.url = url; - this.timeout = timeout; - this.batch = batch; - } - - @Override - public ExitStatus execute(Settings settings, Environment env) throws Exception { - PluginManager pluginManager = new PluginManager(env, url, outputMode, timeout); - if (name != null) { - terminal.println("-> Installing " + Strings.coalesceToEmpty(name) + "..."); - } else { - terminal.println("-> Installing from " + URLDecoder.decode(url.toString(), "UTF-8") + "..."); - } - pluginManager.downloadAndExtract(name, terminal, batch); - return ExitStatus.OK; - } - } -} \ No newline at end of file diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java index fd7f2d84e21..4fd039cfaaf 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java @@ -19,6 +19,7 @@ package org.elasticsearch.plugins; +import org.apache.lucene.util.IOUtils; import org.elasticsearch.common.cli.Terminal; import org.elasticsearch.common.cli.Terminal.Verbosity; import org.elasticsearch.env.Environment; @@ -38,7 +39,7 @@ import java.util.Comparator; import java.util.List; class PluginSecurity { - + /** * Reads plugin policy, prints/confirms exceptions */ @@ -49,7 +50,7 @@ class PluginSecurity { terminal.print(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions"); return; } - + // sort permissions in a reasonable order Collections.sort(requested, new Comparator() { @Override @@ -80,7 +81,7 @@ class PluginSecurity { return cmp; } }); - + terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); terminal.println(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @"); terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); @@ -98,11 +99,11 @@ class PluginSecurity { } } } - + /** Format permission type, name, and actions into a string */ static String formatPermission(Permission permission) { StringBuilder sb = new StringBuilder(); - + String clazz = null; if (permission instanceof UnresolvedPermission) { clazz = ((UnresolvedPermission) permission).getUnresolvedType(); @@ -110,7 +111,7 @@ class PluginSecurity { clazz = permission.getClass().getName(); } sb.append(clazz); - + String name = null; if (permission instanceof UnresolvedPermission) { name = ((UnresolvedPermission) permission).getUnresolvedName(); @@ -121,7 +122,7 @@ class PluginSecurity { sb.append(' '); sb.append(name); } - + String actions = null; if (permission instanceof UnresolvedPermission) { actions = ((UnresolvedPermission) permission).getUnresolvedActions(); @@ -134,7 +135,7 @@ class PluginSecurity { } return sb.toString(); } - + /** * Parses plugin policy into a set of permissions */ @@ -151,8 +152,8 @@ class PluginSecurity { } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } - PluginManager.tryToDeletePath(terminal, emptyPolicyFile); - + IOUtils.rm(emptyPolicyFile); + // parse the plugin's policy file into a set of permissions final Policy policy; try { diff --git a/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java new file mode 100644 index 00000000000..f5e55ba2b24 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE; + +/** + * A command for the plugin cli to remove a plugin from elasticsearch. + */ +class RemovePluginCommand extends CliTool.Command { + private final String pluginName; + + public RemovePluginCommand(Terminal terminal, String pluginName) { + super(terminal); + this.pluginName = pluginName; + } + + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + terminal.println("-> Removing " + Strings.coalesceToEmpty(pluginName) + "..."); + + Path pluginDir = env.pluginsFile().resolve(pluginName); + if (Files.exists(pluginDir) == false) { + throw new IllegalArgumentException("Plugin " + pluginName + " not found. Run 'plugin list' to get list of installed plugins."); + } + + List pluginPaths = new ArrayList<>(); + + Path pluginBinDir = env.binFile().resolve(pluginName); + if (Files.exists(pluginBinDir)) { + if (Files.isDirectory(pluginBinDir) == false) { + throw new IllegalStateException("Bin dir for " + pluginName + " is not a directory"); + } + pluginPaths.add(pluginBinDir); + terminal.println(VERBOSE, "Removing: %s", pluginBinDir); + } + + terminal.println(VERBOSE, "Removing: %s", pluginDir); + Path tmpPluginDir = env.pluginsFile().resolve(".removing-" + pluginName); + Files.move(pluginDir, tmpPluginDir, StandardCopyOption.ATOMIC_MOVE); + pluginPaths.add(tmpPluginDir); + + IOUtils.rm(pluginPaths.toArray(new Path[pluginPaths.size()])); + + return CliTool.ExitStatus.OK; + } +} diff --git a/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help b/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help index 9d5b8b3d68d..b3e9d36a613 100644 --- a/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help +++ b/core/src/main/resources/org/elasticsearch/plugins/plugin-install.help @@ -13,16 +13,11 @@ DESCRIPTION Officially supported or commercial plugins require just the plugin name: plugin install analysis-icu - plugin install shield + plugin install x-pack - Plugins from GitHub require 'username/repository' or 'username/repository/version': + Plugins from Maven Central require 'groupId:artifactId:version': - plugin install lmenezes/elasticsearch-kopf - plugin install lmenezes/elasticsearch-kopf/1.5.7 - - Plugins from Maven Central or Sonatype require 'groupId/artifactId/version': - - plugin install org.elasticsearch/elasticsearch-mapper-attachments/2.6.0 + plugin install org.elasticsearch:mapper-attachments:3.0.0 Plugins can be installed from a custom URL or file location as follows: @@ -58,8 +53,6 @@ OFFICIAL PLUGINS OPTIONS - -t,--timeout Timeout until the plugin download is abort - -v,--verbose Verbose output -h,--help Shows this message diff --git a/core/src/test/java/org/elasticsearch/common/io/FileSystemUtilsTests.java b/core/src/test/java/org/elasticsearch/common/io/FileSystemUtilsTests.java index 4f2b8f6811c..b6266773bf0 100644 --- a/core/src/test/java/org/elasticsearch/common/io/FileSystemUtilsTests.java +++ b/core/src/test/java/org/elasticsearch/common/io/FileSystemUtilsTests.java @@ -48,91 +48,6 @@ public class FileSystemUtilsTests extends ESTestCase { dst = createTempDir(); Files.createDirectories(src); Files.createDirectories(dst); - - // We first copy sources test files from src/test/resources - // Because after when the test runs, src files are moved to their destination - final Path path = getDataPath("/org/elasticsearch/common/io/copyappend"); - FileSystemUtils.copyDirectoryRecursively(path, src); - } - - public void testMoveOverExistingFileAndAppend() throws IOException { - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v1"), dst, ".new"); - assertFileContent(dst, "file1.txt", "version1"); - assertFileContent(dst, "dir/file2.txt", "version1"); - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v2"), dst, ".new"); - assertFileContent(dst, "file1.txt", "version1"); - assertFileContent(dst, "dir/file2.txt", "version1"); - assertFileContent(dst, "file1.txt.new", "version2"); - assertFileContent(dst, "dir/file2.txt.new", "version2"); - assertFileContent(dst, "file3.txt", "version1"); - assertFileContent(dst, "dir/subdir/file4.txt", "version1"); - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v3"), dst, ".new"); - assertFileContent(dst, "file1.txt", "version1"); - assertFileContent(dst, "dir/file2.txt", "version1"); - assertFileContent(dst, "file1.txt.new", "version3"); - assertFileContent(dst, "dir/file2.txt.new", "version3"); - assertFileContent(dst, "file3.txt", "version1"); - assertFileContent(dst, "dir/subdir/file4.txt", "version1"); - assertFileContent(dst, "file3.txt.new", "version2"); - assertFileContent(dst, "dir/subdir/file4.txt.new", "version2"); - assertFileContent(dst, "dir/subdir/file5.txt", "version1"); - } - - public void testMoveOverExistingFileAndIgnore() throws IOException { - Path dest = createTempDir(); - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v1"), dest, null); - assertFileContent(dest, "file1.txt", "version1"); - assertFileContent(dest, "dir/file2.txt", "version1"); - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v2"), dest, null); - assertFileContent(dest, "file1.txt", "version1"); - assertFileContent(dest, "dir/file2.txt", "version1"); - assertFileContent(dest, "file1.txt.new", null); - assertFileContent(dest, "dir/file2.txt.new", null); - assertFileContent(dest, "file3.txt", "version1"); - assertFileContent(dest, "dir/subdir/file4.txt", "version1"); - - FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v3"), dest, null); - assertFileContent(dest, "file1.txt", "version1"); - assertFileContent(dest, "dir/file2.txt", "version1"); - assertFileContent(dest, "file1.txt.new", null); - assertFileContent(dest, "dir/file2.txt.new", null); - assertFileContent(dest, "file3.txt", "version1"); - assertFileContent(dest, "dir/subdir/file4.txt", "version1"); - assertFileContent(dest, "file3.txt.new", null); - assertFileContent(dest, "dir/subdir/file4.txt.new", null); - assertFileContent(dest, "dir/subdir/file5.txt", "version1"); - } - - public void testMoveFilesDoesNotCreateSameFileWithSuffix() throws Exception { - Path[] dirs = new Path[] { createTempDir(), createTempDir(), createTempDir()}; - for (Path dir : dirs) { - Files.write(dir.resolve("file1.txt"), "file1".getBytes(StandardCharsets.UTF_8)); - Files.createDirectory(dir.resolve("dir")); - Files.write(dir.resolve("dir").resolve("file2.txt"), "file2".getBytes(StandardCharsets.UTF_8)); - } - - FileSystemUtils.moveFilesWithoutOverwriting(dirs[0], dst, ".new"); - assertFileContent(dst, "file1.txt", "file1"); - assertFileContent(dst, "dir/file2.txt", "file2"); - - // do the same operation again, make sure, no .new files have been added - FileSystemUtils.moveFilesWithoutOverwriting(dirs[1], dst, ".new"); - assertFileContent(dst, "file1.txt", "file1"); - assertFileContent(dst, "dir/file2.txt", "file2"); - assertFileNotExists(dst.resolve("file1.txt.new")); - assertFileNotExists(dst.resolve("dir").resolve("file2.txt.new")); - - // change file content, make sure it gets updated - Files.write(dirs[2].resolve("dir").resolve("file2.txt"), "UPDATED".getBytes(StandardCharsets.UTF_8)); - FileSystemUtils.moveFilesWithoutOverwriting(dirs[2], dst, ".new"); - assertFileContent(dst, "file1.txt", "file1"); - assertFileContent(dst, "dir/file2.txt", "file2"); - assertFileContent(dst, "dir/file2.txt.new", "UPDATED"); } public void testAppend() { diff --git a/core/src/test/java/org/elasticsearch/plugins/PluginManagerCliTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java similarity index 78% rename from core/src/test/java/org/elasticsearch/plugins/PluginManagerCliTests.java rename to core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java index f16f9981d93..99d1c33821b 100644 --- a/core/src/test/java/org/elasticsearch/plugins/PluginManagerCliTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java @@ -32,25 +32,25 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; -public class PluginManagerCliTests extends CliToolTestCase { +public class PluginCliTests extends CliToolTestCase { public void testHelpWorks() throws IOException { CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(); - assertThat(new PluginManagerCliParser(terminal).execute(args("--help")), is(OK_AND_EXIT)); + assertThat(new PluginCli(terminal).execute(args("--help")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin.help"); terminal.getTerminalOutput().clear(); - assertThat(new PluginManagerCliParser(terminal).execute(args("install -h")), is(OK_AND_EXIT)); + assertThat(new PluginCli(terminal).execute(args("install -h")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-install.help"); - for (String plugin : PluginManager.OFFICIAL_PLUGINS) { + for (String plugin : InstallPluginCommand.OFFICIAL_PLUGINS) { assertThat(terminal.getTerminalOutput(), hasItem(containsString(plugin))); } terminal.getTerminalOutput().clear(); - assertThat(new PluginManagerCliParser(terminal).execute(args("remove --help")), is(OK_AND_EXIT)); + assertThat(new PluginCli(terminal).execute(args("remove --help")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-remove.help"); terminal.getTerminalOutput().clear(); - assertThat(new PluginManagerCliParser(terminal).execute(args("list -h")), is(OK_AND_EXIT)); + assertThat(new PluginCli(terminal).execute(args("list -h")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-list.help"); } @@ -58,8 +58,7 @@ public class PluginManagerCliTests extends CliToolTestCase { CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(); Path tmpDir = createTempDir().resolve("foo deps"); String finalDir = tmpDir.toAbsolutePath().toUri().toURL().toString(); - logger.warn(finalDir); - CliTool.ExitStatus execute = new PluginManagerCliParser(terminal).execute(args("install " + finalDir)); + CliTool.ExitStatus execute = new PluginCli(terminal).execute("install", finalDir); assertThat(execute.status(), is(IO_ERROR.status())); } } diff --git a/core/src/test/java/org/elasticsearch/plugins/loading/classpath/InClassPathPlugin.java b/core/src/test/java/org/elasticsearch/plugins/loading/classpath/InClassPathPlugin.java deleted file mode 100644 index 79b1f244eff..00000000000 --- a/core/src/test/java/org/elasticsearch/plugins/loading/classpath/InClassPathPlugin.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugins.loading.classpath; - -import org.elasticsearch.plugins.Plugin; - -public class InClassPathPlugin extends Plugin { - - @Override - public String name() { - return "in-classpath-plugin"; - } - - @Override - public String description() { - return "A plugin defined in class path"; - } -} diff --git a/core/src/test/resources/org/elasticsearch/plugins/loading/classpath/es-plugin-test.properties b/core/src/test/resources/org/elasticsearch/plugins/loading/classpath/es-plugin-test.properties deleted file mode 100644 index f57bea58cf2..00000000000 --- a/core/src/test/resources/org/elasticsearch/plugins/loading/classpath/es-plugin-test.properties +++ /dev/null @@ -1,19 +0,0 @@ -################################################################ -# Licensed to Elasticsearch under one or more contributor -# license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright -# ownership. Elasticsearch licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -################################################################ -plugin=org.elasticsearch.plugins.loading.classpath.InClassPathPlugin \ No newline at end of file diff --git a/distribution/src/main/resources/bin/plugin b/distribution/src/main/resources/bin/plugin index 95011870358..1bab4b1118c 100755 --- a/distribution/src/main/resources/bin/plugin +++ b/distribution/src/main/resources/bin/plugin @@ -110,4 +110,4 @@ fi HOSTNAME=`hostname | cut -d. -f1` export HOSTNAME -eval "$JAVA" -client -Delasticsearch -Des.path.home="\"$ES_HOME\"" $properties -cp "\"$ES_HOME/lib/*\"" org.elasticsearch.plugins.PluginManagerCliParser $args +eval "$JAVA" -client -Delasticsearch -Des.path.home="\"$ES_HOME\"" $properties -cp "\"$ES_HOME/lib/*\"" org.elasticsearch.plugins.PluginCli $args diff --git a/distribution/src/main/resources/bin/plugin.bat b/distribution/src/main/resources/bin/plugin.bat index c41b01566366b73a8bf69607ee59bd35c888dbee..6c6be019fc67034f69de3227ebf927a55e2d12a9 100644 GIT binary patch delta 15 WcmaFHd6RQPE(^1BPUhrVmP`OJBn68A delta 28 jcmcb~`HXWzE{lL~VqRi;YLRnJW permissions; + public PosixPermissionsResetter(Path path) throws IOException { + attributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class); + assertNotNull(attributeView); + permissions = attributeView.readAttributes().permissions(); + } + @Override + public void close() throws IOException { + attributeView.setPermissions(permissions); + } + public void setPermissions(Set newPermissions) throws IOException { + attributeView.setPermissions(newPermissions); + } + } + + /** Creates a test environment with bin, config and plugins directories. */ + static Environment createEnv() throws IOException { + Path home = createTempDir(); + Files.createDirectories(home.resolve("bin")); + Files.createFile(home.resolve("bin").resolve("elasticsearch")); + Files.createDirectories(home.resolve("config")); + Files.createFile(home.resolve("config").resolve("elasticsearch.yml")); + Files.createDirectories(home.resolve("plugins")); + Settings settings = Settings.builder() + .put("path.home", home) + .build(); + return new Environment(settings); + } + + /** creates a fake jar file with empty class files */ + static void writeJar(Path jar, String... classes) throws IOException { + try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(jar))) { + for (String clazz : classes) { + stream.putNextEntry(new ZipEntry(clazz + ".class")); // no package names, just support simple classes + } + } + } + + static String writeZip(Path structure) throws IOException { + Path zip = createTempDir().resolve(structure.getFileName() + ".zip"); + try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) { + Files.walkFileTree(structure, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + stream.putNextEntry(new ZipEntry(structure.relativize(file).toString())); + Files.copy(file, stream); + return FileVisitResult.CONTINUE; + } + }); + } + return zip.toUri().toURL().toString(); + } + + /** creates a plugin .zip and returns the url for testing */ + static String createPlugin(String name, Path structure) throws IOException { + PluginTestUtil.writeProperties(structure, + "description", "fake desc", + "name", name, + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin"); + writeJar(structure.resolve("plugin.jar"), "FakePlugin"); + return writeZip(structure); + } + + static CliToolTestCase.CaptureOutputTerminal installPlugin(String pluginUrl, Environment env) throws Exception { + CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.NORMAL); + CliTool.ExitStatus status = new InstallPluginCommand(terminal, pluginUrl, true).execute(env.settings(), env); + assertEquals(CliTool.ExitStatus.OK, status); + return terminal; + } + + void assertPlugin(String name, Path original, Environment env) throws IOException { + Path got = env.pluginsFile().resolve(name); + assertTrue("dir " + name + " exists", Files.exists(got)); + assertTrue("jar was copied", Files.exists(got.resolve("plugin.jar"))); + assertFalse("bin was not copied", Files.exists(got.resolve("bin"))); + assertFalse("config was not copied", Files.exists(got.resolve("config"))); + if (Files.exists(original.resolve("bin"))) { + Path binDir = env.binFile().resolve(name); + assertTrue("bin dir exists", Files.exists(binDir)); + assertTrue("bin is a dir", Files.isDirectory(binDir)); + PosixFileAttributes binAttributes = null; + if (isPosix) { + binAttributes = Files.readAttributes(env.binFile(), PosixFileAttributes.class); + } + try (DirectoryStream stream = Files.newDirectoryStream(binDir)) { + for (Path file : stream) { + assertFalse("not a dir", Files.isDirectory(file)); + if (isPosix) { + PosixFileAttributes attributes = Files.readAttributes(file, PosixFileAttributes.class); + Set expectedPermissions = new HashSet<>(binAttributes.permissions()); + expectedPermissions.add(PosixFilePermission.OWNER_EXECUTE); + expectedPermissions.add(PosixFilePermission.GROUP_EXECUTE); + expectedPermissions.add(PosixFilePermission.OTHERS_EXECUTE); + assertEquals(expectedPermissions, attributes.permissions()); + } + } + } + } + if (Files.exists(original.resolve("config"))) { + Path configDir = env.configFile().resolve(name); + assertTrue("config dir exists", Files.exists(configDir)); + assertTrue("config is a dir", Files.isDirectory(configDir)); + try (DirectoryStream stream = Files.newDirectoryStream(configDir)) { + for (Path file : stream) { + assertFalse("not a dir", Files.isDirectory(file)); + } + } + } + assertInstallCleaned(env); + } + + void assertInstallCleaned(Environment env) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(env.pluginsFile())) { + for (Path file : stream) { + if (file.getFileName().toString().startsWith(".installing")) { + fail("Installation dir still exists, " + file); + } + } + } + } + + public void testSomethingWorks() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + String pluginZip = createPlugin("fake", pluginDir); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + } + + public void testPluginsDirMissing() throws Exception { + Environment env = createEnv(); + Files.delete(env.pluginsFile()); + Path pluginDir = createTempDir(); + String pluginZip = createPlugin("fake", pluginDir); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + } + + public void testPluginsDirReadOnly() throws Exception { + assumeTrue("posix filesystem", isPosix); + Environment env = createEnv(); + try (PosixPermissionsResetter pluginsAttrs = new PosixPermissionsResetter(env.pluginsFile())) { + pluginsAttrs.setPermissions(new HashSet<>()); + String pluginZip = createPlugin("fake", createTempDir()); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("Plugins directory is read only")); + } + assertInstallCleaned(env); + } + + public void testBuiltinModule() throws Exception { + Environment env = createEnv(); + String pluginZip = createPlugin("lang-groovy", createTempDir()); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("is a system module")); + assertInstallCleaned(env); + } + + public void testJarHell() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + writeJar(pluginDir.resolve("other.jar"), "FakePlugin"); + String pluginZip = createPlugin("fake", pluginDir); // adds plugin.jar with FakePlugin + IllegalStateException e = expectThrows(IllegalStateException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("jar hell")); + assertInstallCleaned(env); + } + + public void testIsolatedPlugins() throws Exception { + Environment env = createEnv(); + // these both share the same FakePlugin class + Path pluginDir1 = createTempDir(); + String pluginZip1 = createPlugin("fake1", pluginDir1); + installPlugin(pluginZip1, env); + Path pluginDir2 = createTempDir(); + String pluginZip2 = createPlugin("fake2", pluginDir2); + installPlugin(pluginZip2, env); + assertPlugin("fake1", pluginDir1, env); + assertPlugin("fake2", pluginDir2, env); + } + + public void testPurgatoryJarHell() throws Exception { + Environment env = createEnv(); + Path pluginDir1 = createTempDir(); + PluginTestUtil.writeProperties(pluginDir1, + "description", "fake desc", + "name", "fake1", + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin", + "isolated", "false"); + writeJar(pluginDir1.resolve("plugin.jar"), "FakePlugin"); + String pluginZip1 = writeZip(pluginDir1); + installPlugin(pluginZip1, env); + + Path pluginDir2 = createTempDir(); + PluginTestUtil.writeProperties(pluginDir2, + "description", "fake desc", + "name", "fake2", + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin", + "isolated", "false"); + writeJar(pluginDir2.resolve("plugin.jar"), "FakePlugin"); + String pluginZip2 = writeZip(pluginDir2); + IllegalStateException e = expectThrows(IllegalStateException.class, () -> { + installPlugin(pluginZip2, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("jar hell")); + assertInstallCleaned(env); + } + + public void testExistingPlugin() throws Exception { + Environment env = createEnv(); + String pluginZip = createPlugin("fake", createTempDir()); + installPlugin(pluginZip, env); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("already exists")); + assertInstallCleaned(env); + } + + public void testBin() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path binDir = pluginDir.resolve("bin"); + Files.createDirectory(binDir); + Files.createFile(binDir.resolve("somescript")); + String pluginZip = createPlugin("fake", pluginDir); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + } + + public void testBinNotDir() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path binDir = pluginDir.resolve("bin"); + Files.createFile(binDir); + String pluginZip = createPlugin("fake", pluginDir); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); + assertInstallCleaned(env); + } + + public void testBinContainsDir() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path dirInBinDir = pluginDir.resolve("bin").resolve("foo"); + Files.createDirectories(dirInBinDir); + Files.createFile(dirInBinDir.resolve("somescript")); + String pluginZip = createPlugin("fake", pluginDir); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin")); + assertInstallCleaned(env); + } + + public void testBinConflict() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path binDir = pluginDir.resolve("bin"); + Files.createDirectory(binDir); + Files.createFile(binDir.resolve("somescript")); + String pluginZip = createPlugin("elasticsearch", pluginDir); + FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains(env.binFile().resolve("elasticsearch").toString())); + assertInstallCleaned(env); + } + + public void testBinPermissions() throws Exception { + assumeTrue("posix filesystem", isPosix); + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path binDir = pluginDir.resolve("bin"); + Files.createDirectory(binDir); + Files.createFile(binDir.resolve("somescript")); + String pluginZip = createPlugin("fake", pluginDir); + try (PosixPermissionsResetter binAttrs = new PosixPermissionsResetter(env.binFile())) { + Set perms = new HashSet<>(binAttrs.permissions); + // make sure at least one execute perm is missing, so we know we forced it during installation + perms.remove(PosixFilePermission.GROUP_EXECUTE); + binAttrs.setPermissions(perms); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + } + } + + public void testConfig() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path configDir = pluginDir.resolve("config"); + Files.createDirectory(configDir); + Files.createFile(configDir.resolve("custom.yaml")); + String pluginZip = createPlugin("fake", pluginDir); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + } + + public void testExistingConfig() throws Exception { + Environment env = createEnv(); + Path envConfigDir = env.configFile().resolve("fake"); + Files.createDirectories(envConfigDir); + Files.write(envConfigDir.resolve("custom.yaml"), "existing config".getBytes(StandardCharsets.UTF_8)); + Path pluginDir = createTempDir(); + Path configDir = pluginDir.resolve("config"); + Files.createDirectory(configDir); + Files.write(configDir.resolve("custom.yaml"), "new config".getBytes(StandardCharsets.UTF_8)); + Files.createFile(configDir.resolve("other.yaml")); + String pluginZip = createPlugin("fake", pluginDir); + installPlugin(pluginZip, env); + assertPlugin("fake", pluginDir, env); + List configLines = Files.readAllLines(envConfigDir.resolve("custom.yaml"), StandardCharsets.UTF_8); + assertEquals(1, configLines.size()); + assertEquals("existing config", configLines.get(0)); + assertTrue(Files.exists(envConfigDir.resolve("other.yaml"))); + } + + public void testConfigNotDir() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path configDir = pluginDir.resolve("config"); + Files.createFile(configDir); + String pluginZip = createPlugin("fake", pluginDir); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); + assertInstallCleaned(env); + } + + public void testConfigContainsDir() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path dirInConfigDir = pluginDir.resolve("config").resolve("foo"); + Files.createDirectories(dirInConfigDir); + Files.createFile(dirInConfigDir.resolve("myconfig.yml")); + String pluginZip = createPlugin("fake", pluginDir); + IOException e = expectThrows(IOException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in config dir for plugin")); + assertInstallCleaned(env); + } + + public void testConfigConflict() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + Path configDir = pluginDir.resolve("config"); + Files.createDirectory(configDir); + Files.createFile(configDir.resolve("myconfig.yml")); + String pluginZip = createPlugin("elasticsearch.yml", pluginDir); + FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> { + installPlugin(pluginZip, env); + }); + assertTrue(e.getMessage(), e.getMessage().contains(env.configFile().resolve("elasticsearch.yml").toString())); + assertInstallCleaned(env); + } + + // TODO: test batch flag? + // TODO: test checksum (need maven/official below) + // TODO: test maven, official, and staging install...need tests with fixtures... +} diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java new file mode 100644 index 00000000000..c68e207c0c3 --- /dev/null +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.CliToolTestCase; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.test.ESTestCase; + +@LuceneTestCase.SuppressFileSystems("*") +public class ListPluginsCommandTests extends ESTestCase { + + Environment createEnv() throws IOException { + Path home = createTempDir(); + Files.createDirectories(home.resolve("plugins")); + Settings settings = Settings.builder() + .put("path.home", home) + .build(); + return new Environment(settings); + } + + static CliToolTestCase.CaptureOutputTerminal listPlugins(Environment env) throws Exception { + CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.NORMAL); + CliTool.ExitStatus status = new ListPluginsCommand(terminal).execute(env.settings(), env); + assertEquals(CliTool.ExitStatus.OK, status); + return terminal; + } + + public void testPluginsDirMissing() throws Exception { + Environment env = createEnv(); + Files.delete(env.pluginsFile()); + IOException e = expectThrows(IOException.class, () -> { + listPlugins(env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("Plugins directory missing")); + } + + public void testNoPlugins() throws Exception { + CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(createEnv()); + List lines = terminal.getTerminalOutput(); + assertEquals(0, lines.size()); + } + + public void testOnePlugin() throws Exception { + Environment env = createEnv(); + Files.createDirectory(env.pluginsFile().resolve("fake")); + CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(env); + List lines = terminal.getTerminalOutput(); + assertEquals(1, lines.size()); + assertTrue(lines.get(0).contains("fake")); + } + + public void testTwoPlugins() throws Exception { + Environment env = createEnv(); + Files.createDirectory(env.pluginsFile().resolve("fake1")); + Files.createDirectory(env.pluginsFile().resolve("fake2")); + CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(env); + List lines = terminal.getTerminalOutput(); + assertEquals(2, lines.size()); + Collections.sort(lines); + assertTrue(lines.get(0).contains("fake1")); + assertTrue(lines.get(1).contains("fake2")); + } +} diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerPermissionTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerPermissionTests.java deleted file mode 100644 index 5e70cf71923..00000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerPermissionTests.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugins; - -import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.Version; -import org.elasticsearch.common.cli.CliToolTestCase.CaptureOutputTerminal; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.env.Environment; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; - -import java.io.IOException; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFileAttributeView; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.HashSet; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertDirectoryExists; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileNotExists; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - -// there are some lucene file systems that seem to cause problems (deleted files, dirs instead of files) -@LuceneTestCase.SuppressFileSystems("*") -public class PluginManagerPermissionTests extends ESTestCase { - - private String pluginName = "my-plugin"; - private CaptureOutputTerminal terminal = new CaptureOutputTerminal(); - private Environment environment; - private boolean supportsPermissions; - - @Before - public void setup() { - Path tempDir = createTempDir(); - Settings.Builder settingsBuilder = settingsBuilder().put(Environment.PATH_HOME_SETTING.getKey(), tempDir); - if (randomBoolean()) { - settingsBuilder.put(Environment.PATH_PLUGINS_SETTING.getKey(), createTempDir()); - } - - if (randomBoolean()) { - settingsBuilder.put(Environment.PATH_CONF_SETTING.getKey(), createTempDir()); - } - - environment = new Environment(settingsBuilder.build()); - - supportsPermissions = tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"); - } - - public void testThatUnaccessibleBinDirectoryAbortsPluginInstallation() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - URL pluginUrl = createPlugin(true, randomBoolean()); - - Path binPath = environment.binFile().resolve(pluginName); - Files.createDirectories(binPath); - try { - Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("---------")); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - pluginManager.downloadAndExtract(pluginName, terminal, true); - - fail("Expected IOException but did not happen"); - } catch (IOException e) { - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - assertFileNotExists(environment.configFile().resolve(pluginName)); - // exists, because of our weird permissions above - assertDirectoryExists(environment.binFile().resolve(pluginName)); - - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Error copying bin directory "))); - } finally { - Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("rwxrwxrwx")); - } - } - - public void testThatUnaccessiblePluginConfigDirectoryAbortsPluginInstallation() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - URL pluginUrl = createPlugin(randomBoolean(), true); - - Path path = environment.configFile().resolve(pluginName); - Files.createDirectories(path); - Files.createFile(path.resolve("my-custom-config.yaml")); - Path binPath = environment.binFile().resolve(pluginName); - Files.createDirectories(binPath); - - try { - Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("---------")); - Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("---------")); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - pluginManager.downloadAndExtract(pluginName, terminal, true); - - fail("Expected IOException but did not happen, terminal output was " + terminal.getTerminalOutput()); - } catch (IOException e) { - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - assertFileNotExists(environment.binFile().resolve(pluginName)); - // exists, because of our weird permissions above - assertDirectoryExists(environment.configFile().resolve(pluginName)); - - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Error copying config directory "))); - } finally { - Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxrwxrwx")); - Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("rwxrwxrwx")); - } - } - - // config/bin are not writable, but the plugin does not need to put anything into it - public void testThatPluginWithoutBinAndConfigWorksEvenIfPermissionsAreWrong() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - URL pluginUrl = createPlugin(false, false); - Path path = environment.configFile().resolve(pluginName); - Files.createDirectories(path); - Files.createFile(path.resolve("my-custom-config.yaml")); - Path binPath = environment.binFile().resolve(pluginName); - Files.createDirectories(binPath); - - try { - Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("---------")); - Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("---------")); - Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("---------")); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - pluginManager.downloadAndExtract(pluginName, terminal, true); - } finally { - Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("rwxrwxrwx")); - Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxrwxrwx")); - Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("rwxrwxrwx")); - } - - } - - // plugins directory no accessible, should leave no other left over directories - public void testThatNonWritablePluginsDirectoryLeavesNoLeftOver() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - URL pluginUrl = createPlugin(true, true); - Files.createDirectories(environment.pluginsFile()); - - try { - Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("---------")); - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - try { - pluginManager.downloadAndExtract(pluginName, terminal, true); - fail("Expected IOException due to read-only plugins/ directory"); - } catch (IOException e) { - assertFileNotExists(environment.binFile().resolve(pluginName)); - assertFileNotExists(environment.configFile().resolve(pluginName)); - - Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("rwxrwxrwx")); - assertDirectoryExists(environment.pluginsFile()); - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - } - } finally { - Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("rwxrwxrwx")); - } - } - - public void testThatUnwriteableBackupFilesInConfigurationDirectoryAreReplaced() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - boolean pluginContainsExecutables = randomBoolean(); - URL pluginUrl = createPlugin(pluginContainsExecutables, true); - Files.createDirectories(environment.configFile().resolve(pluginName)); - - Path configFile = environment.configFile().resolve(pluginName).resolve("my-custom-config.yaml"); - Files.createFile(configFile); - Path backupConfigFile = environment.configFile().resolve(pluginName).resolve("my-custom-config.yaml.new"); - Files.createFile(backupConfigFile); - Files.write(backupConfigFile, "foo".getBytes(Charset.forName("UTF-8"))); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - try { - Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("---------")); - - pluginManager.downloadAndExtract(pluginName, terminal, true); - - if (pluginContainsExecutables) { - assertDirectoryExists(environment.binFile().resolve(pluginName)); - } - assertDirectoryExists(environment.pluginsFile().resolve(pluginName)); - assertDirectoryExists(environment.configFile().resolve(pluginName)); - - assertFileExists(backupConfigFile); - Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("rw-rw-rw-")); - String content = new String(Files.readAllBytes(backupConfigFile), Charset.forName("UTF-8")); - assertThat(content, is(not("foo"))); - } finally { - Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("rw-rw-rw-")); - } - } - - public void testThatConfigDirectoryBeingAFileAbortsInstallationAndDoesNotAccidentallyDeleteThisFile() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - Files.createDirectories(environment.configFile()); - Files.createFile(environment.configFile().resolve(pluginName)); - URL pluginUrl = createPlugin(randomBoolean(), true); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - - try { - pluginManager.downloadAndExtract(pluginName, terminal, true); - fail("Expected plugin installation to fail, but didnt"); - } catch (IOException e) { - assertFileExists(environment.configFile().resolve(pluginName)); - assertFileNotExists(environment.binFile().resolve(pluginName)); - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - } - } - - public void testThatBinDirectoryBeingAFileAbortsInstallationAndDoesNotAccidentallyDeleteThisFile() throws Exception { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - - Files.createDirectories(environment.binFile()); - Files.createFile(environment.binFile().resolve(pluginName)); - URL pluginUrl = createPlugin(true, randomBoolean()); - - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - - try { - pluginManager.downloadAndExtract(pluginName, terminal, true); - fail("Expected plugin installation to fail, but didnt"); - } catch (IOException e) { - assertFileExists(environment.binFile().resolve(pluginName)); - assertFileNotExists(environment.configFile().resolve(pluginName)); - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - } - } - - public void testConfigDirectoryOwnerGroupAndPermissions() throws IOException { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - URL pluginUrl = createPlugin(false, true); - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - pluginManager.downloadAndExtract(pluginName, terminal, true); - PosixFileAttributes parentFileAttributes = Files.getFileAttributeView(environment.configFile(), PosixFileAttributeView.class).readAttributes(); - Path configPath = environment.configFile().resolve(pluginName); - PosixFileAttributes pluginConfigDirAttributes = Files.getFileAttributeView(configPath, PosixFileAttributeView.class).readAttributes(); - assertThat(pluginConfigDirAttributes.owner(), equalTo(parentFileAttributes.owner())); - assertThat(pluginConfigDirAttributes.group(), equalTo(parentFileAttributes.group())); - assertThat(pluginConfigDirAttributes.permissions(), equalTo(parentFileAttributes.permissions())); - Path configFile = configPath.resolve("my-custom-config.yaml"); - PosixFileAttributes pluginConfigFileAttributes = Files.getFileAttributeView(configFile, PosixFileAttributeView.class).readAttributes(); - assertThat(pluginConfigFileAttributes.owner(), equalTo(parentFileAttributes.owner())); - assertThat(pluginConfigFileAttributes.group(), equalTo(parentFileAttributes.group())); - Set expectedFilePermissions = new HashSet<>(); - for (PosixFilePermission parentPermission : parentFileAttributes.permissions()) { - switch(parentPermission) { - case OWNER_EXECUTE: - case GROUP_EXECUTE: - case OTHERS_EXECUTE: - break; - default: - expectedFilePermissions.add(parentPermission); - } - } - assertThat(pluginConfigFileAttributes.permissions(), equalTo(expectedFilePermissions)); - } - - public void testBinDirectoryOwnerGroupAndPermissions() throws IOException { - assumeTrue("File system does not support permissions, skipping", supportsPermissions); - URL pluginUrl = createPlugin(true, false); - PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10)); - pluginManager.downloadAndExtract(pluginName, terminal, true); - PosixFileAttributes parentFileAttributes = Files.getFileAttributeView(environment.binFile(), PosixFileAttributeView.class).readAttributes(); - Path binPath = environment.binFile().resolve(pluginName); - PosixFileAttributes pluginBinDirAttributes = Files.getFileAttributeView(binPath, PosixFileAttributeView.class).readAttributes(); - assertThat(pluginBinDirAttributes.owner(), equalTo(parentFileAttributes.owner())); - assertThat(pluginBinDirAttributes.group(), equalTo(parentFileAttributes.group())); - assertThat(pluginBinDirAttributes.permissions(), equalTo(parentFileAttributes.permissions())); - Path executableFile = binPath.resolve("my-binary"); - PosixFileAttributes pluginExecutableFileAttributes = Files.getFileAttributeView(executableFile, PosixFileAttributeView.class).readAttributes(); - assertThat(pluginExecutableFileAttributes.owner(), equalTo(parentFileAttributes.owner())); - assertThat(pluginExecutableFileAttributes.group(), equalTo(parentFileAttributes.group())); - Set expectedFilePermissions = new HashSet<>(); - expectedFilePermissions.add(OWNER_EXECUTE); - expectedFilePermissions.add(GROUP_EXECUTE); - expectedFilePermissions.add(OTHERS_EXECUTE); - for (PosixFilePermission parentPermission : parentFileAttributes.permissions()) { - switch(parentPermission) { - case OWNER_EXECUTE: - case GROUP_EXECUTE: - case OTHERS_EXECUTE: - break; - default: - expectedFilePermissions.add(parentPermission); - } - } - - assertThat(pluginExecutableFileAttributes.permissions(), equalTo(expectedFilePermissions)); - } - - private URL createPlugin(boolean withBinDir, boolean withConfigDir) throws IOException { - final Path structure = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(structure, "description", "fake desc", - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "jvm", "true", - "java.version", "1.7", - "name", pluginName, - "classname", pluginName); - if (withBinDir) { - // create bin dir - Path binDir = structure.resolve("bin"); - Files.createDirectory(binDir); - Files.setPosixFilePermissions(binDir, PosixFilePermissions.fromString("rwxr-xr-x")); - - // create executable - Path executable = binDir.resolve("my-binary"); - Files.createFile(executable); - Files.setPosixFilePermissions(executable, PosixFilePermissions.fromString("rw-r--r--")); - } - if (withConfigDir) { - // create bin dir - Path configDir = structure.resolve("config"); - Files.createDirectory(configDir); - Files.setPosixFilePermissions(configDir, PosixFilePermissions.fromString("rwxr-xr-x")); - - // create config file - Path configFile = configDir.resolve("my-custom-config.yaml"); - Files.createFile(configFile); - Files.write(configFile, "my custom config content".getBytes(Charset.forName("UTF-8"))); - Files.setPosixFilePermissions(configFile, PosixFilePermissions.fromString("rw-r--r--")); - } - - Path zip = createTempDir().resolve(structure.getFileName() + ".zip"); - try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) { - Files.walkFileTree(structure, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - stream.putNextEntry(new ZipEntry(structure.relativize(file).toString())); - Files.copy(file, stream); - return FileVisitResult.CONTINUE; - } - }); - } - return zip.toUri().toURL(); - } -} diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java deleted file mode 100644 index d997a167541..00000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerTests.java +++ /dev/null @@ -1,725 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.elasticsearch.plugins; - -import org.apache.http.impl.client.HttpClients; -import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.Version; -import org.elasticsearch.common.Base64; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.cli.CliTool; -import org.elasticsearch.common.cli.CliTool.ExitStatus; -import org.elasticsearch.common.cli.CliToolTestCase.CaptureOutputTerminal; -import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.node.internal.InternalSettingsPreparer; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.ESIntegTestCase.ClusterScope; -import org.elasticsearch.test.junit.annotations.Network; -import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; -import org.elasticsearch.test.rest.client.http.HttpResponse; -import org.jboss.netty.bootstrap.ServerBootstrap; -import org.jboss.netty.channel.Channel; -import org.jboss.netty.channel.ChannelHandlerContext; -import org.jboss.netty.channel.ChannelPipeline; -import org.jboss.netty.channel.ChannelPipelineFactory; -import org.jboss.netty.channel.Channels; -import org.jboss.netty.channel.MessageEvent; -import org.jboss.netty.channel.SimpleChannelUpstreamHandler; -import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; -import org.jboss.netty.handler.codec.http.DefaultHttpResponse; -import org.jboss.netty.handler.codec.http.HttpRequest; -import org.jboss.netty.handler.codec.http.HttpRequestDecoder; -import org.jboss.netty.handler.codec.http.HttpResponseEncoder; -import org.jboss.netty.handler.codec.http.HttpResponseStatus; -import org.jboss.netty.handler.ssl.SslContext; -import org.jboss.netty.handler.ssl.SslHandler; -import org.jboss.netty.handler.ssl.util.InsecureTrustManagerFactory; -import org.jboss.netty.handler.ssl.util.SelfSignedCertificate; -import org.junit.After; -import org.junit.Before; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import java.io.BufferedWriter; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFileAttributeView; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import static org.elasticsearch.common.cli.CliTool.ExitStatus.USAGE; -import static org.elasticsearch.common.cli.CliToolTestCase.args; -import static org.elasticsearch.common.io.FileTestUtils.assertFileContent; -import static org.elasticsearch.common.settings.Settings.settingsBuilder; -import static org.elasticsearch.test.ESIntegTestCase.Scope; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertDirectoryExists; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileNotExists; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; - -@ClusterScope(scope = Scope.TEST, numDataNodes = 0, transportClientRatio = 0.0) -@LuceneTestCase.SuppressFileSystems("*") // TODO: clean up this test to allow extra files -// TODO: jimfs is really broken here (throws wrong exception from detection method). -// if its in your classpath, then do not use plugins!!!!!! -@SuppressForbidden(reason = "modifies system properties intentionally") -public class PluginManagerTests extends ESIntegTestCase { - - private Environment environment; - private CaptureOutputTerminal terminal = new CaptureOutputTerminal(); - - @Before - public void setup() throws Exception { - environment = buildInitialSettings(); - System.setProperty("es.default.path.home", Environment.PATH_HOME_SETTING.get(environment.settings())); - Path binDir = environment.binFile(); - if (!Files.exists(binDir)) { - Files.createDirectories(binDir); - } - Path configDir = environment.configFile(); - if (!Files.exists(configDir)) { - Files.createDirectories(configDir); - } - } - - @After - public void clearPathHome() { - System.clearProperty("es.default.path.home"); - } - - private void writeSha1(Path file, boolean corrupt) throws IOException { - String sha1Hex = MessageDigests.toHexString(MessageDigests.sha1().digest(Files.readAllBytes(file))); - try (BufferedWriter out = Files.newBufferedWriter(file.resolveSibling(file.getFileName() + ".sha1"), StandardCharsets.UTF_8)) { - out.write(sha1Hex); - if (corrupt) { - out.write("bad"); - } - } - } - - private void writeMd5(Path file, boolean corrupt) throws IOException { - String md5Hex = MessageDigests.toHexString(MessageDigests.md5().digest(Files.readAllBytes(file))); - try (BufferedWriter out = Files.newBufferedWriter(file.resolveSibling(file.getFileName() + ".md5"), StandardCharsets.UTF_8)) { - out.write(md5Hex); - if (corrupt) { - out.write("bad"); - } - } - } - - /** creates a plugin .zip and returns the url for testing */ - private String createPlugin(final Path structure, String... properties) throws IOException { - PluginTestUtil.writeProperties(structure, properties); - Path zip = createTempDir().resolve(structure.getFileName() + ".zip"); - try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) { - Files.walkFileTree(structure, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - stream.putNextEntry(new ZipEntry(structure.relativize(file).toString())); - Files.copy(file, stream); - return FileVisitResult.CONTINUE; - } - }); - } - if (randomBoolean()) { - writeSha1(zip, false); - } else if (randomBoolean()) { - writeMd5(zip, false); - } - return zip.toUri().toURL().toString(); - } - - /** creates a plugin .zip and bad checksum file and returns the url for testing */ - private String createPluginWithBadChecksum(final Path structure, String... properties) throws IOException { - PluginTestUtil.writeProperties(structure, properties); - Path zip = createTempDir().resolve(structure.getFileName() + ".zip"); - try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) { - Files.walkFileTree(structure, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - stream.putNextEntry(new ZipEntry(structure.relativize(file).toString())); - Files.copy(file, stream); - return FileVisitResult.CONTINUE; - } - }); - } - if (randomBoolean()) { - writeSha1(zip, true); - } else { - writeMd5(zip, true); - } - return zip.toUri().toURL().toString(); - } - - public void testThatPluginNameMustBeSupplied() throws IOException { - Path pluginDir = createTempDir().resolve("fake-plugin"); - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", "fake-plugin", - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - assertStatus("install", USAGE); - } - - public void testLocalPluginInstallWithBinAndConfig() throws Exception { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - // create bin/tool and config/file - Files.createDirectories(pluginDir.resolve("bin")); - Files.createFile(pluginDir.resolve("bin").resolve("tool")); - Files.createDirectories(pluginDir.resolve("config")); - Files.createFile(pluginDir.resolve("config").resolve("file")); - - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - - Path binDir = environment.binFile(); - Path pluginBinDir = binDir.resolve(pluginName); - - Path pluginConfigDir = environment.configFile().resolve(pluginName); - assertStatusOk("install " + pluginUrl + " --verbose"); - - terminal.getTerminalOutput().clear(); - assertStatusOk("list"); - assertThat(terminal.getTerminalOutput(), hasItem(containsString(pluginName))); - - assertDirectoryExists(pluginBinDir); - assertDirectoryExists(pluginConfigDir); - Path toolFile = pluginBinDir.resolve("tool"); - assertFileExists(toolFile); - - // check that the file is marked executable, without actually checking that we can execute it. - PosixFileAttributeView view = Files.getFileAttributeView(toolFile, PosixFileAttributeView.class); - // the view might be null, on e.g. windows, there is nothing to check there! - if (view != null) { - PosixFileAttributes attributes = view.readAttributes(); - assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_EXECUTE)); - assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_READ)); - } - } - - /** - * Test for #7890 - */ - public void testLocalPluginInstallWithBinAndConfigInAlreadyExistingConfigDir_7890() throws Exception { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - // create config/test.txt with contents 'version1' - Files.createDirectories(pluginDir.resolve("config")); - Files.write(pluginDir.resolve("config").resolve("test.txt"), "version1".getBytes(StandardCharsets.UTF_8)); - - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - - Path pluginConfigDir = environment.configFile().resolve(pluginName); - - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl)); - - /* - First time, our plugin contains: - - config/test.txt (version1) - */ - assertFileContent(pluginConfigDir, "test.txt", "version1"); - - // We now remove the plugin - assertStatusOk("remove " + pluginName); - - // We should still have test.txt - assertFileContent(pluginConfigDir, "test.txt", "version1"); - - // Installing a new plugin version - /* - Second time, our plugin contains: - - config/test.txt (version2) - - config/dir/testdir.txt (version1) - - config/dir/subdir/testsubdir.txt (version1) - */ - Files.write(pluginDir.resolve("config").resolve("test.txt"), "version2".getBytes(StandardCharsets.UTF_8)); - Files.createDirectories(pluginDir.resolve("config").resolve("dir").resolve("subdir")); - Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir.txt"), "version1".getBytes(StandardCharsets.UTF_8)); - Files.write(pluginDir.resolve("config").resolve("dir").resolve("subdir").resolve("testsubdir.txt"), "version1".getBytes(StandardCharsets.UTF_8)); - pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "2.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl)); - - assertFileContent(pluginConfigDir, "test.txt", "version1"); - assertFileContent(pluginConfigDir, "test.txt.new", "version2"); - assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1"); - assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1"); - - // Removing - assertStatusOk("remove " + pluginName); - assertFileContent(pluginConfigDir, "test.txt", "version1"); - assertFileContent(pluginConfigDir, "test.txt.new", "version2"); - assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1"); - assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1"); - - // Installing a new plugin version - /* - Third time, our plugin contains: - - config/test.txt (version3) - - config/test2.txt (version1) - - config/dir/testdir.txt (version2) - - config/dir/testdir2.txt (version1) - - config/dir/subdir/testsubdir.txt (version2) - */ - Files.write(pluginDir.resolve("config").resolve("test.txt"), "version3".getBytes(StandardCharsets.UTF_8)); - Files.write(pluginDir.resolve("config").resolve("test2.txt"), "version1".getBytes(StandardCharsets.UTF_8)); - Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir.txt"), "version2".getBytes(StandardCharsets.UTF_8)); - Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir2.txt"), "version1".getBytes(StandardCharsets.UTF_8)); - Files.write(pluginDir.resolve("config").resolve("dir").resolve("subdir").resolve("testsubdir.txt"), "version2".getBytes(StandardCharsets.UTF_8)); - pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "3.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "jvm", "true", - "classname", "FakePlugin"); - - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl)); - - assertFileContent(pluginConfigDir, "test.txt", "version1"); - assertFileContent(pluginConfigDir, "test2.txt", "version1"); - assertFileContent(pluginConfigDir, "test.txt.new", "version3"); - assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1"); - assertFileContent(pluginConfigDir, "dir/testdir.txt.new", "version2"); - assertFileContent(pluginConfigDir, "dir/testdir2.txt", "version1"); - assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1"); - assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt.new", "version2"); - } - - // For #7152 - public void testLocalPluginInstallWithBinOnly_7152() throws Exception { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - // create bin/tool - Files.createDirectories(pluginDir.resolve("bin")); - Files.createFile(pluginDir.resolve("bin").resolve("tool"));; - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", "fake-plugin", - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - - Path binDir = environment.binFile(); - Path pluginBinDir = binDir.resolve(pluginName); - - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl)); - assertThatPluginIsListed(pluginName); - assertDirectoryExists(pluginBinDir); - } - - public void testListInstalledEmpty() throws IOException { - assertStatusOk("list"); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("No plugin detected"))); - } - - public void testListInstalledEmptyWithExistingPluginDirectory() throws IOException { - Files.createDirectory(environment.pluginsFile()); - assertStatusOk("list"); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("No plugin detected"))); - } - - public void testInstallPluginVerbose() throws IOException { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - System.err.println("install " + pluginUrl + " --verbose"); - ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl + " --verbose")); - assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(ExitStatus.OK)); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Name: fake-plugin"))); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Description: fake desc"))); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Version: 1.0"))); - assertThatPluginIsListed(pluginName); - } - - public void testInstallPlugin() throws IOException { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl)); - assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(ExitStatus.OK)); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Name: fake-plugin")))); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Description:")))); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Site:")))); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Version:")))); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("JVM:")))); - assertThatPluginIsListed(pluginName); - } - - /** - * @deprecated support for this is not going to stick around, seriously. - */ - @Deprecated - public void testAlreadyInstalledNotIsolated() throws Exception { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - Files.createDirectories(pluginDir); - // create a jar file in the plugin - Path pluginJar = pluginDir.resolve("fake-plugin.jar"); - try (ZipOutputStream out = new JarOutputStream(Files.newOutputStream(pluginJar, StandardOpenOption.CREATE))) { - out.putNextEntry(new ZipEntry("foo.class")); - out.closeEntry(); - } - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "isolated", "false", - "classname", "FakePlugin"); - - // install - ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl)); - assertEquals("unexpected exit status: output: " + terminal.getTerminalOutput(), ExitStatus.OK, status); - - // install again - status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl)); - List output = terminal.getTerminalOutput(); - assertEquals("unexpected exit status: output: " + output, ExitStatus.IO_ERROR, status); - boolean foundExpectedMessage = false; - for (String line : output) { - foundExpectedMessage |= line.contains("already exists"); - } - assertTrue(foundExpectedMessage); - } - - public void testInstallPluginWithBadChecksum() throws IOException { - String pluginName = "fake-plugin"; - Path pluginDir = createTempDir().resolve(pluginName); - String pluginUrl = createPluginWithBadChecksum(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - assertStatus(String.format(Locale.ROOT, "install %s --verbose", pluginUrl), - ExitStatus.IO_ERROR); - assertThatPluginIsNotListed(pluginName); - assertFileNotExists(environment.pluginsFile().resolve(pluginName)); - } - - private void singlePluginInstallAndRemove(String pluginDescriptor, String pluginName, String pluginCoordinates) throws IOException { - logger.info("--> trying to download and install [{}]", pluginDescriptor); - if (pluginCoordinates == null) { - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginDescriptor)); - } else { - assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginCoordinates)); - } - assertThatPluginIsListed(pluginName); - - terminal.getTerminalOutput().clear(); - assertStatusOk("remove " + pluginDescriptor); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Removing " + pluginDescriptor))); - - // not listed anymore - terminal.getTerminalOutput().clear(); - assertStatusOk("list"); - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString(pluginName)))); - } - - /** - * We are ignoring by default these tests as they require to have an internet access - * To activate the test, use -Dtests.network=true - * We test regular form: username/reponame/version - * It should find it in download.elasticsearch.org service - */ - @Network - @AwaitsFix(bugUrl = "fails with jar hell failures - http://build-us-00.elastic.co/job/es_core_master_oracle_6/519/testReport/") - public void testInstallPluginWithElasticsearchDownloadService() throws IOException { - assumeTrue("download.elastic.co is accessible", isDownloadServiceWorking("download.elastic.co", 80, "/elasticsearch/ci-test.txt")); - singlePluginInstallAndRemove("elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null); - } - - /** - * We are ignoring by default these tests as they require to have an internet access - * To activate the test, use -Dtests.network=true - * We test regular form: groupId/artifactId/version - * It should find it in maven central service - */ - @Network - @AwaitsFix(bugUrl = "fails with jar hell failures - http://build-us-00.elastic.co/job/es_core_master_oracle_6/519/testReport/") - public void testInstallPluginWithMavenCentral() throws IOException { - assumeTrue("search.maven.org is accessible", isDownloadServiceWorking("search.maven.org", 80, "/")); - assumeTrue("repo1.maven.org is accessible", isDownloadServiceWorking("repo1.maven.org", 443, "/maven2/org/elasticsearch/elasticsearch-transport-thrift/2.4.0/elasticsearch-transport-thrift-2.4.0.pom")); - singlePluginInstallAndRemove("org.elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null); - } - - /** - * We are ignoring by default these tests as they require to have an internet access - * To activate the test, use -Dtests.network=true - * We test site plugins from github: userName/repoName - * It should find it on github - */ - @Network @AwaitsFix(bugUrl = "needs to be adapted to 2.0") - public void testInstallPluginWithGithub() throws IOException { - assumeTrue("github.com is accessible", isDownloadServiceWorking("github.com", 443, "/")); - singlePluginInstallAndRemove("elasticsearch/kibana", "kibana", null); - } - - private boolean isDownloadServiceWorking(String host, int port, String resource) { - try { - String protocol = port == 443 ? "https" : "http"; - HttpResponse response = new HttpRequestBuilder(HttpClients.createDefault()).protocol(protocol).host(host).port(port).path(resource).execute(); - if (response.getStatusCode() != 200) { - logger.warn("[{}{}] download service is not working. Disabling current test.", host, resource); - return false; - } - return true; - } catch (Throwable t) { - logger.warn("[{}{}] download service is not working. Disabling current test.", host, resource); - } - return false; - } - - public void testRemovePlugin() throws Exception { - String pluginName = "plugintest"; - Path pluginDir = createTempDir().resolve(pluginName); - String pluginUrl = createPlugin(pluginDir, - "description", "fake desc", - "name", pluginName, - "version", "1.0.0", - "elasticsearch.version", Version.CURRENT.toString(), - "java.version", System.getProperty("java.specification.version"), - "classname", "FakePlugin"); - - // We want to remove plugin with plugin short name - singlePluginInstallAndRemove("plugintest", "plugintest", pluginUrl); - - // We want to remove plugin with groupid/artifactid/version form - singlePluginInstallAndRemove("groupid/plugintest/1.0.0", "plugintest", pluginUrl); - - // We want to remove plugin with groupid/artifactid form - singlePluginInstallAndRemove("groupid/plugintest", "plugintest", pluginUrl); - } - - public void testRemovePlugin_NullName_ThrowsException() throws IOException { - assertStatus("remove ", USAGE); - } - - public void testRemovePluginWithURLForm() throws Exception { - assertStatus("remove file://whatever", USAGE); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Illegal plugin name"))); - } - - public void testForbiddenPluginNames() throws IOException { - assertStatus("remove elasticsearch", USAGE); - assertStatus("remove elasticsearch.bat", USAGE); - assertStatus("remove elasticsearch.in.sh", USAGE); - assertStatus("remove plugin", USAGE); - assertStatus("remove plugin.bat", USAGE); - assertStatus("remove service.bat", USAGE); - assertStatus("remove ELASTICSEARCH", USAGE); - assertStatus("remove ELASTICSEARCH.IN.SH", USAGE); - } - - public void testOfficialPluginName_ThrowsException() throws IOException { - PluginManager.checkForOfficialPlugins("analysis-icu"); - PluginManager.checkForOfficialPlugins("analysis-kuromoji"); - PluginManager.checkForOfficialPlugins("analysis-phonetic"); - PluginManager.checkForOfficialPlugins("analysis-smartcn"); - PluginManager.checkForOfficialPlugins("analysis-stempel"); - PluginManager.checkForOfficialPlugins("delete-by-query"); - PluginManager.checkForOfficialPlugins("lang-javascript"); - PluginManager.checkForOfficialPlugins("lang-painless"); - PluginManager.checkForOfficialPlugins("lang-python"); - PluginManager.checkForOfficialPlugins("mapper-attachments"); - PluginManager.checkForOfficialPlugins("mapper-murmur3"); - PluginManager.checkForOfficialPlugins("mapper-size"); - PluginManager.checkForOfficialPlugins("discovery-multicast"); - PluginManager.checkForOfficialPlugins("discovery-azure"); - PluginManager.checkForOfficialPlugins("discovery-ec2"); - PluginManager.checkForOfficialPlugins("discovery-gce"); - PluginManager.checkForOfficialPlugins("repository-azure"); - PluginManager.checkForOfficialPlugins("repository-s3"); - PluginManager.checkForOfficialPlugins("store-smb"); - - try { - PluginManager.checkForOfficialPlugins("elasticsearch-mapper-attachment"); - fail("elasticsearch-mapper-attachment should not be allowed"); - } catch (IllegalArgumentException e) { - // We expect that error - } - } - - public void testThatBasicAuthIsRejectedOnHttp() throws Exception { - assertStatus(String.format(Locale.ROOT, "install http://user:pass@localhost:12345/foo.zip --verbose"), CliTool.ExitStatus.IO_ERROR); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("Basic auth is only supported for HTTPS!"))); - } - - public void testThatBasicAuthIsSupportedWithHttps() throws Exception { - assumeTrue("test requires security manager to be disabled", System.getSecurityManager() == null); - - SSLSocketFactory defaultSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); - ServerBootstrap serverBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory()); - SelfSignedCertificate ssc = null; - - try { - try { - ssc = new SelfSignedCertificate("localhost"); - } catch (Exception e) { - assumeNoException("self signing shenanigans not supported by this JDK", e); - } - - // Create a trust manager that does not validate certificate chains: - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null); - HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); - - final List requests = new ArrayList<>(); - final SslContext sslContext = SslContext.newServerContext(ssc.certificate(), ssc.privateKey()); - - serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() { - @Override - public ChannelPipeline getPipeline() throws Exception { - return Channels.pipeline( - new SslHandler(sslContext.newEngine()), - new HttpRequestDecoder(), - new HttpResponseEncoder(), - new LoggingServerHandler(requests) - ); - } - }); - - Channel channel = serverBootstrap.bind(new InetSocketAddress(InetAddress.getByName("localhost"), 0)); - int port = ((InetSocketAddress) channel.getLocalAddress()).getPort(); - // IO_ERROR because there is no real file delivered... - assertStatus(String.format(Locale.ROOT, "install https://user:pass@localhost:%s/foo.zip --verbose --timeout 10s", port), ExitStatus.IO_ERROR); - - // ensure that we did not try any other data source like download.elastic.co, in case we specified our own local URL - assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("download.elastic.co")))); - - assertThat(requests, hasSize(1)); - String msg = String.format(Locale.ROOT, "Request header did not contain Authorization header, terminal output was: %s", terminal.getTerminalOutput()); - assertThat(msg, requests.get(0).headers().contains("Authorization"), is(true)); - assertThat(msg, requests.get(0).headers().get("Authorization"), is("Basic " + Base64.encodeBytes("user:pass".getBytes(StandardCharsets.UTF_8)))); - } finally { - HttpsURLConnection.setDefaultSSLSocketFactory(defaultSocketFactory); - serverBootstrap.releaseExternalResources(); - if (ssc != null) { - ssc.delete(); - } - } - } - - private static class LoggingServerHandler extends SimpleChannelUpstreamHandler { - - private List requests; - - public LoggingServerHandler(List requests) { - this.requests = requests; - } - - @Override - public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws InterruptedException { - final HttpRequest request = (HttpRequest) e.getMessage(); - requests.add(request); - final org.jboss.netty.handler.codec.http.HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST); - ctx.getChannel().write(response); - } - } - - - - private Environment buildInitialSettings() throws IOException { - Settings settings = settingsBuilder() - .put("http.enabled", true) - .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build(); - return InternalSettingsPreparer.prepareEnvironment(settings, null); - } - - private void assertStatusOk(String command) { - assertStatus(command, ExitStatus.OK); - } - - private void assertStatus(String command, ExitStatus exitStatus) { - ExitStatus status = new PluginManagerCliParser(terminal).execute(args(command)); - assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(exitStatus)); - } - - private void assertThatPluginIsListed(String pluginName) { - terminal.getTerminalOutput().clear(); - assertStatusOk("list"); - String message = String.format(Locale.ROOT, "Terminal output was: %s", terminal.getTerminalOutput()); - assertThat(message, terminal.getTerminalOutput(), hasItem(containsString(pluginName))); - } - - private void assertThatPluginIsNotListed(String pluginName) { - terminal.getTerminalOutput().clear(); - assertStatusOk("list"); - String message = String.format(Locale.ROOT, "Terminal output was: %s", terminal.getTerminalOutput()); - assertFalse(message, terminal.getTerminalOutput().contains(pluginName)); - } -} diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerUnitTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerUnitTests.java deleted file mode 100644 index 49edcc7b1d4..00000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginManagerUnitTests.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.plugins; - -import org.elasticsearch.Build; -import org.elasticsearch.Version; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.http.client.HttpDownloadHelper; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.test.ESTestCase; -import org.junit.After; - -import java.io.IOException; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.Locale; - -import static org.elasticsearch.common.settings.Settings.settingsBuilder; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; - -/** - * - */ -@SuppressForbidden(reason = "modifies system properties intentionally") -public class PluginManagerUnitTests extends ESTestCase { - @After - public void cleanSystemProperty() { - System.clearProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS); - } - - public void testThatConfigDirectoryCanBeOutsideOfElasticsearchHomeDirectory() throws IOException { - String pluginName = randomAsciiOfLength(10); - Path homeFolder = createTempDir(); - Path genericConfigFolder = createTempDir(); - - Settings settings = settingsBuilder() - .put(Environment.PATH_CONF_SETTING.getKey(), genericConfigFolder) - .put(Environment.PATH_HOME_SETTING.getKey(), homeFolder) - .build(); - Environment environment = new Environment(settings); - - PluginManager.PluginHandle pluginHandle = new PluginManager.PluginHandle(pluginName, "version", "user"); - Path configDirPath = pluginHandle.configDir(environment).normalize(); - Path expectedDirPath = genericConfigFolder.resolve(pluginName).normalize(); - assertEquals(configDirPath, expectedDirPath); - } - - public void testSimplifiedNaming() throws IOException { - String pluginName = randomAsciiOfLength(10); - PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(pluginName); - - boolean supportStagingUrls = randomBoolean(); - if (supportStagingUrls) { - System.setProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS, "true"); - } - - Iterator iterator = handle.urls().iterator(); - - if (supportStagingUrls) { - String expectedStagingURL = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip", - Version.CURRENT.number(), Build.CURRENT.shortHash(), pluginName, Version.CURRENT.number(), pluginName, Version.CURRENT.number()); - assertThat(iterator.next().toExternalForm(), is(expectedStagingURL)); - } - - URL expected = new URL("https", "download.elastic.co", "/elasticsearch/release/org/elasticsearch/plugin/" + pluginName + "/" + Version.CURRENT.number() + "/" + - pluginName + "-" + Version.CURRENT.number() + ".zip"); - assertThat(iterator.next().toExternalForm(), is(expected.toExternalForm())); - - assertThat(iterator.hasNext(), is(false)); - } - - public void testOfficialPluginName() throws IOException { - String randomPluginName = randomFrom(new ArrayList<>(PluginManager.OFFICIAL_PLUGINS)); - PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(randomPluginName); - assertThat(handle.name, is(randomPluginName)); - - boolean supportStagingUrls = randomBoolean(); - if (supportStagingUrls) { - System.setProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS, "true"); - } - - Iterator iterator = handle.urls().iterator(); - - if (supportStagingUrls) { - String expectedStagingUrl = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip", - Version.CURRENT.number(), Build.CURRENT.shortHash(), randomPluginName, Version.CURRENT.number(), randomPluginName, Version.CURRENT.number()); - assertThat(iterator.next().toExternalForm(), is(expectedStagingUrl)); - } - - String releaseUrl = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%s/%s/%s-%s.zip", - randomPluginName, Version.CURRENT.number(), randomPluginName, Version.CURRENT.number()); - assertThat(iterator.next().toExternalForm(), is(releaseUrl)); - - assertThat(iterator.hasNext(), is(false)); - } - - public void testGithubPluginName() throws IOException { - String user = randomAsciiOfLength(6); - String pluginName = randomAsciiOfLength(10); - PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(user + "/" + pluginName); - assertThat(handle.name, is(pluginName)); - assertThat(handle.urls(), hasSize(1)); - assertThat(handle.urls().get(0).toExternalForm(), is(new URL("https", "github.com", "/" + user + "/" + pluginName + "/" + "archive/master.zip").toExternalForm())); - } - - public void testDownloadHelperChecksums() throws Exception { - // Sanity check to make sure the checksum functions never change how they checksum things - assertEquals("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", - HttpDownloadHelper.SHA1_CHECKSUM.checksum("foo".getBytes(Charset.forName("UTF-8")))); - assertEquals("acbd18db4cc2f85cedef654fccc4a4d8", - HttpDownloadHelper.MD5_CHECKSUM.checksum("foo".getBytes(Charset.forName("UTF-8")))); - } -} diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java new file mode 100644 index 00000000000..a643c835bd1 --- /dev/null +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.CliToolTestCase; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.test.ESTestCase; + +@LuceneTestCase.SuppressFileSystems("*") +public class RemovePluginCommandTests extends ESTestCase { + + /** Creates a test environment with bin, config and plugins directories. */ + static Environment createEnv() throws IOException { + Path home = createTempDir(); + Files.createDirectories(home.resolve("bin")); + Files.createFile(home.resolve("bin").resolve("elasticsearch")); + Files.createDirectories(home.resolve("plugins")); + Settings settings = Settings.builder() + .put("path.home", home) + .build(); + return new Environment(settings); + } + + static CliToolTestCase.CaptureOutputTerminal removePlugin(String name, Environment env) throws Exception { + CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.VERBOSE); + CliTool.ExitStatus status = new RemovePluginCommand(terminal, name).execute(env.settings(), env); + assertEquals(CliTool.ExitStatus.OK, status); + return terminal; + } + + static void assertRemoveCleaned(Environment env) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(env.pluginsFile())) { + for (Path file : stream) { + if (file.getFileName().toString().startsWith(".removing")) { + fail("Removal dir still exists, " + file); + } + } + } + } + + public void testMissing() throws Exception { + Environment env = createEnv(); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> { + removePlugin("dne", env); + }); + assertTrue(e.getMessage(), e.getMessage().contains("Plugin dne not found")); + assertRemoveCleaned(env); + } + + public void testBasic() throws Exception { + Environment env = createEnv(); + Files.createDirectory(env.pluginsFile().resolve("fake")); + Files.createFile(env.pluginsFile().resolve("fake").resolve("plugin.jar")); + Files.createDirectory(env.pluginsFile().resolve("fake").resolve("subdir")); + Files.createDirectory(env.pluginsFile().resolve("other")); + removePlugin("fake", env); + assertFalse(Files.exists(env.pluginsFile().resolve("fake"))); + assertTrue(Files.exists(env.pluginsFile().resolve("other"))); + assertRemoveCleaned(env); + } + + public void testBin() throws Exception { + Environment env = createEnv(); + Files.createDirectories(env.pluginsFile().resolve("fake")); + Path binDir = env.binFile().resolve("fake"); + Files.createDirectories(binDir); + Files.createFile(binDir.resolve("somescript")); + removePlugin("fake", env); + assertFalse(Files.exists(env.pluginsFile().resolve("fake"))); + assertTrue(Files.exists(env.binFile().resolve("elasticsearch"))); + assertFalse(Files.exists(binDir)); + assertRemoveCleaned(env); + } + + public void testBinNotDir() throws Exception { + Environment env = createEnv(); + Files.createDirectories(env.pluginsFile().resolve("elasticsearch")); + IllegalStateException e = expectThrows(IllegalStateException.class, () -> { + removePlugin("elasticsearch", env); + }); + assertTrue(Files.exists(env.pluginsFile().resolve("elasticsearch"))); // did not remove + assertTrue(Files.exists(env.binFile().resolve("elasticsearch"))); + assertRemoveCleaned(env); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java b/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java index ab304c28c54..5dfaef187ad 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java @@ -64,8 +64,6 @@ public abstract class CliToolTestCase extends ESTestCase { */ public static class MockTerminal extends Terminal { - private static final PrintWriter DEV_NULL = new PrintWriter(new DevNullWriter()); - public MockTerminal() { super(Verbosity.NORMAL); } @@ -93,29 +91,7 @@ public abstract class CliToolTestCase extends ESTestCase { } @Override - public void printStackTrace(Throwable t) { - return; - } - - @Override - public PrintWriter writer() { - return DEV_NULL; - } - - private static class DevNullWriter extends Writer { - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - } - - @Override - public void flush() throws IOException { - } - - @Override - public void close() throws IOException { - } - } + public void printStackTrace(Throwable t) {} } /** From aa5f9d51586caa98a1156e4160668ce3b8bb2b28 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 1 Feb 2016 11:19:19 +0100 Subject: [PATCH 06/49] Convert PageCacheRecycler settings --- .../cache/recycler/PageCacheRecycler.java | 30 ++++++++++--------- .../common/settings/ClusterSettings.java | 9 +++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/cache/recycler/PageCacheRecycler.java b/core/src/main/java/org/elasticsearch/cache/recycler/PageCacheRecycler.java index 9fbbb151d6a..7016718b302 100644 --- a/core/src/main/java/org/elasticsearch/cache/recycler/PageCacheRecycler.java +++ b/core/src/main/java/org/elasticsearch/cache/recycler/PageCacheRecycler.java @@ -25,7 +25,9 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.recycler.AbstractRecyclerC; import org.elasticsearch.common.recycler.Recycler; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.threadpool.ThreadPool; @@ -41,9 +43,13 @@ import static org.elasticsearch.common.recycler.Recyclers.none; /** A recycler of fixed-size pages. */ public class PageCacheRecycler extends AbstractComponent implements Releasable { - public static final String TYPE = "recycler.page.type"; - public static final String LIMIT_HEAP = "recycler.page.limit.heap"; - public static final String WEIGHT = "recycler.page.weight"; + public static final Setting TYPE_SETTING = new Setting<>("cache.recycler.page.type", Type.CONCURRENT.name(), Type::parse, false, Setting.Scope.CLUSTER); + public static final Setting LIMIT_HEAP_SETTING = Setting.byteSizeSetting("cache.recycler.page.limit.heap", "10%", false, Setting.Scope.CLUSTER); + public static final Setting WEIGHT_BYTES_SETTING = Setting.doubleSetting("cache.recycler.page.weight.bytes", 1d, 0d, false, Setting.Scope.CLUSTER); + public static final Setting WEIGHT_LONG_SETTING = Setting.doubleSetting("cache.recycler.page.weight.longs", 1d, 0d, false, Setting.Scope.CLUSTER); + public static final Setting WEIGHT_INT_SETTING = Setting.doubleSetting("cache.recycler.page.weight.ints", 1d, 0d, false, Setting.Scope.CLUSTER); + // object pages are less useful to us so we give them a lower weight by default + public static final Setting WEIGHT_OBJECTS_SETTING = Setting.doubleSetting("cache.recycler.page.weight.objects", 0.1d, 0d, false, Setting.Scope.CLUSTER); private final Recycler bytePage; private final Recycler intPage; @@ -73,8 +79,8 @@ public class PageCacheRecycler extends AbstractComponent implements Releasable { @Inject public PageCacheRecycler(Settings settings, ThreadPool threadPool) { super(settings); - final Type type = Type.parse(settings.get(TYPE)); - final long limit = settings.getAsMemory(LIMIT_HEAP, "10%").bytes(); + final Type type = TYPE_SETTING .get(settings); + final long limit = LIMIT_HEAP_SETTING .get(settings).bytes(); final int availableProcessors = EsExecutors.boundedNumberOfProcessors(settings); final int searchThreadPoolSize = maximumSearchThreadPoolSize(threadPool, settings); @@ -91,11 +97,10 @@ public class PageCacheRecycler extends AbstractComponent implements Releasable { // to direct ByteBuffers or sun.misc.Unsafe on a byte[] but this would have other issues // that would need to be addressed such as garbage collection of native memory or safety // of Unsafe writes. - final double bytesWeight = settings.getAsDouble(WEIGHT + ".bytes", 1d); - final double intsWeight = settings.getAsDouble(WEIGHT + ".ints", 1d); - final double longsWeight = settings.getAsDouble(WEIGHT + ".longs", 1d); - // object pages are less useful to us so we give them a lower weight by default - final double objectsWeight = settings.getAsDouble(WEIGHT + ".objects", 0.1d); + final double bytesWeight = WEIGHT_BYTES_SETTING .get(settings); + final double intsWeight = WEIGHT_INT_SETTING .get(settings); + final double longsWeight = WEIGHT_LONG_SETTING .get(settings); + final double objectsWeight = WEIGHT_OBJECTS_SETTING .get(settings); final double totalWeight = bytesWeight + intsWeight + longsWeight + objectsWeight; final int maxPageCount = (int) Math.min(Integer.MAX_VALUE, limit / BigArrays.PAGE_SIZE_IN_BYTES); @@ -190,7 +195,7 @@ public class PageCacheRecycler extends AbstractComponent implements Releasable { return recycler; } - public static enum Type { + public enum Type { QUEUE { @Override Recycler build(Recycler.C c, int limit, int estimatedThreadPoolSize, int availableProcessors) { @@ -211,9 +216,6 @@ public class PageCacheRecycler extends AbstractComponent implements Releasable { }; public static Type parse(String type) { - if (Strings.isNullOrEmpty(type)) { - return CONCURRENT; - } try { return Type.valueOf(type.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { diff --git a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 593d586ba96..aa576c8f462 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -22,6 +22,7 @@ import org.elasticsearch.action.admin.indices.close.TransportCloseIndexAction; import org.elasticsearch.action.support.AutoCreateIndex; import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.action.support.master.TransportMasterNodeReadAction; +import org.elasticsearch.cache.recycler.PageCacheRecycler; import org.elasticsearch.client.Client; import org.elasticsearch.client.transport.TransportClientNodesService; import org.elasticsearch.cluster.ClusterModule; @@ -346,6 +347,12 @@ public final class ClusterSettings extends AbstractScopedSettings { FsService.REFRESH_INTERVAL_SETTING, JvmGcMonitorService.ENABLED_SETTING, JvmGcMonitorService.REFRESH_INTERVAL_SETTING, - JvmGcMonitorService.GC_SETTING + JvmGcMonitorService.GC_SETTING, + PageCacheRecycler.LIMIT_HEAP_SETTING, + PageCacheRecycler.WEIGHT_BYTES_SETTING, + PageCacheRecycler.WEIGHT_INT_SETTING, + PageCacheRecycler.WEIGHT_LONG_SETTING, + PageCacheRecycler.WEIGHT_OBJECTS_SETTING, + PageCacheRecycler.TYPE_SETTING ))); } From 745e8f96e7446aa0036ff5145b0a6cfb148e1131 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Fri, 22 Jan 2016 15:05:44 +0000 Subject: [PATCH 07/49] Moved http settings to the new settings infrastructure The following settings were moved to Setting contents in HttpTransportSettings: * http.cors.allow-origin * http.port * http.publish_port * http.detailed_errors.enabled * http.max_content_length * http.max_chunk_size * http.max_header_size * http.max_initial_line_length The following settings were removed: * http.port * http.netty.port * http.netty.publish_port * http.netty.max_content_length * http.netty.max_chunk_size * http.netty.max_header_size * http.netty.max_initial_line_length --- .../org/elasticsearch/bootstrap/Security.java | 6 +- .../common/settings/ClusterSettings.java | 295 +++++++++--------- .../common/transport/PortsRange.java | 4 + .../http/HttpTransportSettings.java | 53 ++++ .../http/netty/HttpRequestHandler.java | 4 +- .../http/netty/NettyHttpChannel.java | 18 +- .../http/netty/NettyHttpServerTransport.java | 52 +-- .../elasticsearch/rest/support/RestUtils.java | 2 +- .../http/netty/HttpPublishPortIT.java | 4 +- .../http/netty/NettyHttpChannelTests.java | 7 +- .../DetailedErrorsDisabledIT.java | 5 +- .../org/elasticsearch/rest/CorsRegexIT.java | 9 +- 12 files changed, 254 insertions(+), 205 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/http/HttpTransportSettings.java diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Security.java b/core/src/main/java/org/elasticsearch/bootstrap/Security.java index fa3ad3e49c4..b9d2bfda24a 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Security.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Security.java @@ -25,7 +25,7 @@ import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; -import org.elasticsearch.http.netty.NettyHttpServerTransport; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.plugins.PluginInfo; import org.elasticsearch.transport.TransportSettings; @@ -270,9 +270,7 @@ final class Security { static void addBindPermissions(Permissions policy, Settings settings) throws IOException { // http is simple - String httpRange = settings.get("http.netty.port", - settings.get("http.port", - NettyHttpServerTransport.DEFAULT_PORT_RANGE)); + String httpRange = HttpTransportSettings.SETTING_HTTP_PORT.get(settings).getPortRangeString(); // listen is always called with 'localhost' but use wildcard to be sure, no name service is consulted. // see SocketPermission implies() code policy.add(new SocketPermission("*:" + httpRange, "listen,resolve")); diff --git a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index aa576c8f462..ea16c6aabd6 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -57,6 +57,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.gateway.PrimaryShardAllocator; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.http.netty.NettyHttpServerTransport; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.store.IndexStoreConfig; @@ -113,9 +114,9 @@ public final class ClusterSettings extends AbstractScopedSettings { @Override public boolean hasChanged(Settings current, Settings previous) { return current.filter(loggerPredicate).getAsMap().equals(previous.filter(loggerPredicate).getAsMap()) == false; - } + } - @Override + @Override public Settings getValue(Settings current, Settings previous) { Settings.Builder builder = Settings.builder(); builder.put(current.filter(loggerPredicate).getAsMap()); @@ -131,7 +132,7 @@ public final class ClusterSettings extends AbstractScopedSettings { return builder.build(); } - @Override + @Override public void apply(Settings value, Settings current, Settings previous) { for (String key : value.getAsMap().keySet()) { assert loggerPredicate.test(key); @@ -142,90 +143,104 @@ public final class ClusterSettings extends AbstractScopedSettings { } else { ESLoggerFactory.getLogger(component).setLevel(value.get(key)); } - } - } + } + } }; public static Set> BUILT_IN_CLUSTER_SETTINGS = Collections.unmodifiableSet(new HashSet<>( Arrays.asList(AwarenessAllocationDecider.CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING, - TransportClientNodesService.CLIENT_TRANSPORT_NODES_SAMPLER_INTERVAL, // TODO these transport client settings are kind of odd here and should only be valid if we are a transport client - TransportClientNodesService.CLIENT_TRANSPORT_PING_TIMEOUT, - TransportClientNodesService.CLIENT_TRANSPORT_IGNORE_CLUSTER_NAME, - AwarenessAllocationDecider.CLUSTER_ROUTING_ALLOCATION_AWARENESS_FORCE_GROUP_SETTING, - BalancedShardsAllocator.INDEX_BALANCE_FACTOR_SETTING, - BalancedShardsAllocator.SHARD_BALANCE_FACTOR_SETTING, - BalancedShardsAllocator.THRESHOLD_SETTING, - ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE_SETTING, - ConcurrentRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_CLUSTER_CONCURRENT_REBALANCE_SETTING, - EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING, - EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING, - ZenDiscovery.REJOIN_ON_MASTER_GONE_SETTING, - FilterAllocationDecider.CLUSTER_ROUTING_INCLUDE_GROUP_SETTING, - FilterAllocationDecider.CLUSTER_ROUTING_EXCLUDE_GROUP_SETTING, - FilterAllocationDecider.CLUSTER_ROUTING_REQUIRE_GROUP_SETTING, - FsRepository.REPOSITORIES_CHUNK_SIZE_SETTING, - FsRepository.REPOSITORIES_COMPRESS_SETTING, - FsRepository.REPOSITORIES_LOCATION_SETTING, - IndexStoreConfig.INDICES_STORE_THROTTLE_TYPE_SETTING, - IndexStoreConfig.INDICES_STORE_THROTTLE_MAX_BYTES_PER_SEC_SETTING, + TransportClientNodesService.CLIENT_TRANSPORT_NODES_SAMPLER_INTERVAL, // TODO these transport client settings are kind of odd here and should only be valid if we are a transport client + TransportClientNodesService.CLIENT_TRANSPORT_PING_TIMEOUT, + TransportClientNodesService.CLIENT_TRANSPORT_IGNORE_CLUSTER_NAME, + AwarenessAllocationDecider.CLUSTER_ROUTING_ALLOCATION_AWARENESS_FORCE_GROUP_SETTING, + BalancedShardsAllocator.INDEX_BALANCE_FACTOR_SETTING, + BalancedShardsAllocator.SHARD_BALANCE_FACTOR_SETTING, + BalancedShardsAllocator.THRESHOLD_SETTING, + ClusterRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ALLOW_REBALANCE_SETTING, + ConcurrentRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_CLUSTER_CONCURRENT_REBALANCE_SETTING, + EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING, + EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING, + ZenDiscovery.REJOIN_ON_MASTER_GONE_SETTING, + FilterAllocationDecider.CLUSTER_ROUTING_INCLUDE_GROUP_SETTING, + FilterAllocationDecider.CLUSTER_ROUTING_EXCLUDE_GROUP_SETTING, + FilterAllocationDecider.CLUSTER_ROUTING_REQUIRE_GROUP_SETTING, + FsRepository.REPOSITORIES_CHUNK_SIZE_SETTING, + FsRepository.REPOSITORIES_COMPRESS_SETTING, + FsRepository.REPOSITORIES_LOCATION_SETTING, + IndexStoreConfig.INDICES_STORE_THROTTLE_TYPE_SETTING, + IndexStoreConfig.INDICES_STORE_THROTTLE_MAX_BYTES_PER_SEC_SETTING, IndicesQueryCache.INDICES_CACHE_QUERY_SIZE_SETTING, IndicesQueryCache.INDICES_CACHE_QUERY_COUNT_SETTING, - IndicesTTLService.INDICES_TTL_INTERVAL_SETTING, - MappingUpdatedAction.INDICES_MAPPING_DYNAMIC_TIMEOUT_SETTING, - MetaData.SETTING_READ_ONLY_SETTING, - RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING, - RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_STATE_SYNC_SETTING, - RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_NETWORK_SETTING, - RecoverySettings.INDICES_RECOVERY_ACTIVITY_TIMEOUT_SETTING, - RecoverySettings.INDICES_RECOVERY_INTERNAL_ACTION_TIMEOUT_SETTING, - RecoverySettings.INDICES_RECOVERY_INTERNAL_LONG_ACTION_TIMEOUT_SETTING, - ThreadPool.THREADPOOL_GROUP_SETTING, - ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_INITIAL_PRIMARIES_RECOVERIES_SETTING, - ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_INCOMING_RECOVERIES_SETTING, - ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_OUTGOING_RECOVERIES_SETTING, - ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_RECOVERIES_SETTING, - DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING, - DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING, - DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_DISK_THRESHOLD_ENABLED_SETTING, - DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_INCLUDE_RELOCATIONS_SETTING, - DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_REROUTE_INTERVAL_SETTING, - InternalClusterInfoService.INTERNAL_CLUSTER_INFO_UPDATE_INTERVAL_SETTING, - InternalClusterInfoService.INTERNAL_CLUSTER_INFO_TIMEOUT_SETTING, - SnapshotInProgressAllocationDecider.CLUSTER_ROUTING_ALLOCATION_SNAPSHOT_RELOCATION_ENABLED_SETTING, - DestructiveOperations.REQUIRES_NAME_SETTING, - DiscoverySettings.PUBLISH_TIMEOUT_SETTING, - DiscoverySettings.PUBLISH_DIFF_ENABLE_SETTING, - DiscoverySettings.COMMIT_TIMEOUT_SETTING, - DiscoverySettings.NO_MASTER_BLOCK_SETTING, - GatewayService.EXPECTED_DATA_NODES_SETTING, - GatewayService.EXPECTED_MASTER_NODES_SETTING, - GatewayService.EXPECTED_NODES_SETTING, - GatewayService.RECOVER_AFTER_DATA_NODES_SETTING, - GatewayService.RECOVER_AFTER_MASTER_NODES_SETTING, - GatewayService.RECOVER_AFTER_NODES_SETTING, - GatewayService.RECOVER_AFTER_TIME_SETTING, - NetworkModule.HTTP_ENABLED, - NettyHttpServerTransport.SETTING_CORS_ALLOW_CREDENTIALS, - NettyHttpServerTransport.SETTING_CORS_ENABLED, - NettyHttpServerTransport.SETTING_CORS_MAX_AGE, - NettyHttpServerTransport.SETTING_HTTP_DETAILED_ERRORS_ENABLED, - NettyHttpServerTransport.SETTING_PIPELINING, - HierarchyCircuitBreakerService.TOTAL_CIRCUIT_BREAKER_LIMIT_SETTING, - HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_LIMIT_SETTING, - HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_OVERHEAD_SETTING, - HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_LIMIT_SETTING, - HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_OVERHEAD_SETTING, - InternalClusterService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, - SearchService.DEFAULT_SEARCH_TIMEOUT_SETTING, - ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING, - TransportService.TRACE_LOG_EXCLUDE_SETTING, - TransportService.TRACE_LOG_INCLUDE_SETTING, - TransportCloseIndexAction.CLUSTER_INDICES_CLOSE_ENABLE_SETTING, - ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING, - InternalClusterService.CLUSTER_SERVICE_RECONNECT_INTERVAL_SETTING, - HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_TYPE_SETTING, - HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_TYPE_SETTING, - Transport.TRANSPORT_TCP_COMPRESS, + IndicesTTLService.INDICES_TTL_INTERVAL_SETTING, + MappingUpdatedAction.INDICES_MAPPING_DYNAMIC_TIMEOUT_SETTING, + MetaData.SETTING_READ_ONLY_SETTING, + RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING, + RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_STATE_SYNC_SETTING, + RecoverySettings.INDICES_RECOVERY_RETRY_DELAY_NETWORK_SETTING, + RecoverySettings.INDICES_RECOVERY_ACTIVITY_TIMEOUT_SETTING, + RecoverySettings.INDICES_RECOVERY_INTERNAL_ACTION_TIMEOUT_SETTING, + RecoverySettings.INDICES_RECOVERY_INTERNAL_LONG_ACTION_TIMEOUT_SETTING, + ThreadPool.THREADPOOL_GROUP_SETTING, + ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_INITIAL_PRIMARIES_RECOVERIES_SETTING, + ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_INCOMING_RECOVERIES_SETTING, + ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_OUTGOING_RECOVERIES_SETTING, + ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_RECOVERIES_SETTING, + DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING, + DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING, + DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_DISK_THRESHOLD_ENABLED_SETTING, + DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_INCLUDE_RELOCATIONS_SETTING, + DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_REROUTE_INTERVAL_SETTING, + InternalClusterInfoService.INTERNAL_CLUSTER_INFO_UPDATE_INTERVAL_SETTING, + InternalClusterInfoService.INTERNAL_CLUSTER_INFO_TIMEOUT_SETTING, + SnapshotInProgressAllocationDecider.CLUSTER_ROUTING_ALLOCATION_SNAPSHOT_RELOCATION_ENABLED_SETTING, + DestructiveOperations.REQUIRES_NAME_SETTING, + DiscoverySettings.PUBLISH_TIMEOUT_SETTING, + DiscoverySettings.PUBLISH_DIFF_ENABLE_SETTING, + DiscoverySettings.COMMIT_TIMEOUT_SETTING, + DiscoverySettings.NO_MASTER_BLOCK_SETTING, + GatewayService.EXPECTED_DATA_NODES_SETTING, + GatewayService.EXPECTED_MASTER_NODES_SETTING, + GatewayService.EXPECTED_NODES_SETTING, + GatewayService.RECOVER_AFTER_DATA_NODES_SETTING, + GatewayService.RECOVER_AFTER_MASTER_NODES_SETTING, + GatewayService.RECOVER_AFTER_NODES_SETTING, + GatewayService.RECOVER_AFTER_TIME_SETTING, + NetworkModule.HTTP_ENABLED, + HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS, + HttpTransportSettings.SETTING_CORS_ENABLED, + HttpTransportSettings.SETTING_CORS_MAX_AGE, + HttpTransportSettings.SETTING_HTTP_DETAILED_ERRORS_ENABLED, + HttpTransportSettings.SETTING_PIPELINING, + HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN, + HttpTransportSettings.SETTING_HTTP_PORT, + HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT, + HttpTransportSettings.SETTING_PIPELINING_MAX_EVENTS, + HttpTransportSettings.SETTING_HTTP_COMPRESSION, + HttpTransportSettings.SETTING_HTTP_COMPRESSION_LEVEL, + HttpTransportSettings.SETTING_CORS_ALLOW_METHODS, + HttpTransportSettings.SETTING_CORS_ALLOW_HEADERS, + HttpTransportSettings.SETTING_HTTP_DETAILED_ERRORS_ENABLED, + HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH, + HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE, + HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE, + HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH, + HttpTransportSettings.SETTING_HTTP_RESET_COOKIES, + HierarchyCircuitBreakerService.TOTAL_CIRCUIT_BREAKER_LIMIT_SETTING, + HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_LIMIT_SETTING, + HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_OVERHEAD_SETTING, + HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_LIMIT_SETTING, + HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_OVERHEAD_SETTING, + InternalClusterService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, + SearchService.DEFAULT_SEARCH_TIMEOUT_SETTING, + ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING, + TransportService.TRACE_LOG_EXCLUDE_SETTING, + TransportService.TRACE_LOG_INCLUDE_SETTING, + TransportCloseIndexAction.CLUSTER_INDICES_CLOSE_ENABLE_SETTING, + ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING, + InternalClusterService.CLUSTER_SERVICE_RECONNECT_INTERVAL_SETTING, + HierarchyCircuitBreakerService.FIELDDATA_CIRCUIT_BREAKER_TYPE_SETTING, + HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_TYPE_SETTING, + Transport.TRANSPORT_TCP_COMPRESS, TransportSettings.TRANSPORT_PROFILES_SETTING, TransportSettings.HOST, TransportSettings.PUBLISH_HOST, @@ -254,72 +269,72 @@ public final class ClusterSettings extends AbstractScopedSettings { NettyTransport.TCP_SEND_BUFFER_SIZE, NettyTransport.TCP_RECEIVE_BUFFER_SIZE, NettyTransport.TCP_BLOCKING_SERVER, - NetworkService.GLOBAL_NETWORK_HOST_SETTING, - NetworkService.GLOBAL_NETWORK_BINDHOST_SETTING, - NetworkService.GLOBAL_NETWORK_PUBLISHHOST_SETTING, - NetworkService.TcpSettings.TCP_NO_DELAY, - NetworkService.TcpSettings.TCP_KEEP_ALIVE, - NetworkService.TcpSettings.TCP_REUSE_ADDRESS, - NetworkService.TcpSettings.TCP_SEND_BUFFER_SIZE, - NetworkService.TcpSettings.TCP_RECEIVE_BUFFER_SIZE, - NetworkService.TcpSettings.TCP_BLOCKING, - NetworkService.TcpSettings.TCP_BLOCKING_SERVER, - NetworkService.TcpSettings.TCP_BLOCKING_CLIENT, - NetworkService.TcpSettings.TCP_CONNECT_TIMEOUT, - IndexSettings.QUERY_STRING_ANALYZE_WILDCARD, - IndexSettings.QUERY_STRING_ALLOW_LEADING_WILDCARD, - PrimaryShardAllocator.NODE_INITIAL_SHARDS_SETTING, - ScriptService.SCRIPT_CACHE_SIZE_SETTING, - IndicesFieldDataCache.INDICES_FIELDDATA_CLEAN_INTERVAL_SETTING, - IndicesFieldDataCache.INDICES_FIELDDATA_CACHE_SIZE_KEY, - IndicesRequestCache.INDICES_CACHE_QUERY_SIZE, - IndicesRequestCache.INDICES_CACHE_QUERY_EXPIRE, + NetworkService.GLOBAL_NETWORK_HOST_SETTING, + NetworkService.GLOBAL_NETWORK_BINDHOST_SETTING, + NetworkService.GLOBAL_NETWORK_PUBLISHHOST_SETTING, + NetworkService.TcpSettings.TCP_NO_DELAY, + NetworkService.TcpSettings.TCP_KEEP_ALIVE, + NetworkService.TcpSettings.TCP_REUSE_ADDRESS, + NetworkService.TcpSettings.TCP_SEND_BUFFER_SIZE, + NetworkService.TcpSettings.TCP_RECEIVE_BUFFER_SIZE, + NetworkService.TcpSettings.TCP_BLOCKING, + NetworkService.TcpSettings.TCP_BLOCKING_SERVER, + NetworkService.TcpSettings.TCP_BLOCKING_CLIENT, + NetworkService.TcpSettings.TCP_CONNECT_TIMEOUT, + IndexSettings.QUERY_STRING_ANALYZE_WILDCARD, + IndexSettings.QUERY_STRING_ALLOW_LEADING_WILDCARD, + PrimaryShardAllocator.NODE_INITIAL_SHARDS_SETTING, + ScriptService.SCRIPT_CACHE_SIZE_SETTING, + IndicesFieldDataCache.INDICES_FIELDDATA_CLEAN_INTERVAL_SETTING, + IndicesFieldDataCache.INDICES_FIELDDATA_CACHE_SIZE_KEY, + IndicesRequestCache.INDICES_CACHE_QUERY_SIZE, + IndicesRequestCache.INDICES_CACHE_QUERY_EXPIRE, IndicesRequestCache.INDICES_CACHE_REQUEST_CLEAN_INTERVAL, - HunspellService.HUNSPELL_LAZY_LOAD, - HunspellService.HUNSPELL_IGNORE_CASE, - HunspellService.HUNSPELL_DICTIONARY_OPTIONS, - IndicesStore.INDICES_STORE_DELETE_SHARD_TIMEOUT, - Environment.PATH_CONF_SETTING, - Environment.PATH_DATA_SETTING, - Environment.PATH_HOME_SETTING, - Environment.PATH_LOGS_SETTING, - Environment.PATH_PLUGINS_SETTING, - Environment.PATH_REPO_SETTING, - Environment.PATH_SCRIPTS_SETTING, - Environment.PATH_SHARED_DATA_SETTING, - Environment.PIDFILE_SETTING, - DiscoveryService.DISCOVERY_SEED_SETTING, - DiscoveryService.INITIAL_STATE_TIMEOUT_SETTING, - DiscoveryModule.DISCOVERY_TYPE_SETTING, - DiscoveryModule.ZEN_MASTER_SERVICE_TYPE_SETTING, - FaultDetection.PING_RETRIES_SETTING, - FaultDetection.PING_TIMEOUT_SETTING, - FaultDetection.REGISTER_CONNECTION_LISTENER_SETTING, - FaultDetection.PING_INTERVAL_SETTING, - FaultDetection.CONNECT_ON_NETWORK_DISCONNECT_SETTING, - ZenDiscovery.PING_TIMEOUT_SETTING, - ZenDiscovery.JOIN_TIMEOUT_SETTING, - ZenDiscovery.JOIN_RETRY_ATTEMPTS_SETTING, - ZenDiscovery.JOIN_RETRY_DELAY_SETTING, - ZenDiscovery.MAX_PINGS_FROM_ANOTHER_MASTER_SETTING, - ZenDiscovery.SEND_LEAVE_REQUEST_SETTING, - ZenDiscovery.MASTER_ELECTION_FILTER_CLIENT_SETTING, - ZenDiscovery.MASTER_ELECTION_WAIT_FOR_JOINS_TIMEOUT_SETTING, - ZenDiscovery.MASTER_ELECTION_FILTER_DATA_SETTING, - UnicastZenPing.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING, - UnicastZenPing.DISCOVERY_ZEN_PING_UNICAST_CONCURRENT_CONNECTS_SETTING, - SearchService.DEFAULT_KEEPALIVE_SETTING, - SearchService.KEEPALIVE_INTERVAL_SETTING, - Node.WRITE_PORTS_FIELD_SETTING, + HunspellService.HUNSPELL_LAZY_LOAD, + HunspellService.HUNSPELL_IGNORE_CASE, + HunspellService.HUNSPELL_DICTIONARY_OPTIONS, + IndicesStore.INDICES_STORE_DELETE_SHARD_TIMEOUT, + Environment.PATH_CONF_SETTING, + Environment.PATH_DATA_SETTING, + Environment.PATH_HOME_SETTING, + Environment.PATH_LOGS_SETTING, + Environment.PATH_PLUGINS_SETTING, + Environment.PATH_REPO_SETTING, + Environment.PATH_SCRIPTS_SETTING, + Environment.PATH_SHARED_DATA_SETTING, + Environment.PIDFILE_SETTING, + DiscoveryService.DISCOVERY_SEED_SETTING, + DiscoveryService.INITIAL_STATE_TIMEOUT_SETTING, + DiscoveryModule.DISCOVERY_TYPE_SETTING, + DiscoveryModule.ZEN_MASTER_SERVICE_TYPE_SETTING, + FaultDetection.PING_RETRIES_SETTING, + FaultDetection.PING_TIMEOUT_SETTING, + FaultDetection.REGISTER_CONNECTION_LISTENER_SETTING, + FaultDetection.PING_INTERVAL_SETTING, + FaultDetection.CONNECT_ON_NETWORK_DISCONNECT_SETTING, + ZenDiscovery.PING_TIMEOUT_SETTING, + ZenDiscovery.JOIN_TIMEOUT_SETTING, + ZenDiscovery.JOIN_RETRY_ATTEMPTS_SETTING, + ZenDiscovery.JOIN_RETRY_DELAY_SETTING, + ZenDiscovery.MAX_PINGS_FROM_ANOTHER_MASTER_SETTING, + ZenDiscovery.SEND_LEAVE_REQUEST_SETTING, + ZenDiscovery.MASTER_ELECTION_FILTER_CLIENT_SETTING, + ZenDiscovery.MASTER_ELECTION_WAIT_FOR_JOINS_TIMEOUT_SETTING, + ZenDiscovery.MASTER_ELECTION_FILTER_DATA_SETTING, + UnicastZenPing.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING, + UnicastZenPing.DISCOVERY_ZEN_PING_UNICAST_CONCURRENT_CONNECTS_SETTING, + SearchService.DEFAULT_KEEPALIVE_SETTING, + SearchService.KEEPALIVE_INTERVAL_SETTING, + Node.WRITE_PORTS_FIELD_SETTING, Node.NODE_CLIENT_SETTING, Node.NODE_DATA_SETTING, Node.NODE_MASTER_SETTING, Node.NODE_LOCAL_SETTING, Node.NODE_MODE_SETTING, Node.NODE_INGEST_SETTING, - URLRepository.ALLOWED_URLS_SETTING, - URLRepository.REPOSITORIES_LIST_DIRECTORIES_SETTING, - URLRepository.REPOSITORIES_URL_SETTING, + URLRepository.ALLOWED_URLS_SETTING, + URLRepository.REPOSITORIES_LIST_DIRECTORIES_SETTING, + URLRepository.REPOSITORIES_URL_SETTING, URLRepository.SUPPORTED_PROTOCOLS_SETTING, TransportMasterNodeReadAction.FORCE_LOCAL_SETTING, AutoCreateIndex.AUTO_CREATE_INDEX_SETTING, diff --git a/core/src/main/java/org/elasticsearch/common/transport/PortsRange.java b/core/src/main/java/org/elasticsearch/common/transport/PortsRange.java index e1e4571eda4..4f5a3966d43 100644 --- a/core/src/main/java/org/elasticsearch/common/transport/PortsRange.java +++ b/core/src/main/java/org/elasticsearch/common/transport/PortsRange.java @@ -35,6 +35,10 @@ public class PortsRange { this.portRange = portRange; } + public String getPortRangeString() { + return portRange; + } + public int[] ports() throws NumberFormatException { final IntArrayList ports = new IntArrayList(); iterate(new PortCallback() { diff --git a/core/src/main/java/org/elasticsearch/http/HttpTransportSettings.java b/core/src/main/java/org/elasticsearch/http/HttpTransportSettings.java new file mode 100644 index 00000000000..c5a1844f7ff --- /dev/null +++ b/core/src/main/java/org/elasticsearch/http/HttpTransportSettings.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.http; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Scope; +import org.elasticsearch.common.transport.PortsRange; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; + +public final class HttpTransportSettings { + + public static final Setting SETTING_CORS_ENABLED = Setting.boolSetting("http.cors.enabled", false, false, Scope.CLUSTER); + public static final Setting SETTING_CORS_ALLOW_ORIGIN = new Setting("http.cors.allow-origin", "", (value) -> value, false, Scope.CLUSTER); + public static final Setting SETTING_CORS_MAX_AGE = Setting.intSetting("http.cors.max-age", 1728000, false, Scope.CLUSTER); + public static final Setting SETTING_CORS_ALLOW_METHODS = new Setting("http.cors.allow-methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE", (value) -> value, false, Scope.CLUSTER); + public static final Setting SETTING_CORS_ALLOW_HEADERS = new Setting("http.cors.allow-headers", "X-Requested-With, Content-Type, Content-Length", (value) -> value, false, Scope.CLUSTER); + public static final Setting SETTING_CORS_ALLOW_CREDENTIALS = Setting.boolSetting("http.cors.allow-credentials", false, false, Scope.CLUSTER); + public static final Setting SETTING_PIPELINING = Setting.boolSetting("http.pipelining", true, false, Scope.CLUSTER); + public static final Setting SETTING_PIPELINING_MAX_EVENTS = Setting.intSetting("http.pipelining.max_events", 10000, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_COMPRESSION = Setting.boolSetting("http.compression", false, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_COMPRESSION_LEVEL = Setting.intSetting("http.compression_level", 6, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_PORT = new Setting("http.port", "9200-9300", PortsRange::new, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_PUBLISH_PORT = Setting.intSetting("http.publish_port", 0, 0, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_DETAILED_ERRORS_ENABLED = Setting.boolSetting("http.detailed_errors.enabled", true, false, Scope.CLUSTER); + public static final Setting SETTING_HTTP_MAX_CONTENT_LENGTH = Setting.byteSizeSetting("http.max_content_length", new ByteSizeValue(100, ByteSizeUnit.MB), false, Scope.CLUSTER) ; + public static final Setting SETTING_HTTP_MAX_CHUNK_SIZE = Setting.byteSizeSetting("http.max_chunk_size", new ByteSizeValue(8, ByteSizeUnit.KB), false, Scope.CLUSTER) ; + public static final Setting SETTING_HTTP_MAX_HEADER_SIZE = Setting.byteSizeSetting("http.max_header_size", new ByteSizeValue(8, ByteSizeUnit.KB), false, Scope.CLUSTER) ; + public static final Setting SETTING_HTTP_MAX_INITIAL_LINE_LENGTH = Setting.byteSizeSetting("http.max_initial_line_length", new ByteSizeValue(4, ByteSizeUnit.KB), false, Scope.CLUSTER) ; + // don't reset cookies by default, since I don't think we really need to + // note, parsing cookies was fixed in netty 3.5.1 regarding stack allocation, but still, currently, we don't need cookies + public static final Setting SETTING_HTTP_RESET_COOKIES = Setting.boolSetting("http.reset_cookies", false, false, Scope.CLUSTER); + + private HttpTransportSettings() { + } +} diff --git a/core/src/main/java/org/elasticsearch/http/netty/HttpRequestHandler.java b/core/src/main/java/org/elasticsearch/http/netty/HttpRequestHandler.java index 71d63d8d1dc..17e14fe83f1 100644 --- a/core/src/main/java/org/elasticsearch/http/netty/HttpRequestHandler.java +++ b/core/src/main/java/org/elasticsearch/http/netty/HttpRequestHandler.java @@ -20,6 +20,7 @@ package org.elasticsearch.http.netty; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.http.netty.pipelining.OrderedUpstreamMessageEvent; import org.elasticsearch.rest.support.RestUtils; import org.jboss.netty.channel.ChannelHandler; @@ -46,7 +47,8 @@ public class HttpRequestHandler extends SimpleChannelUpstreamHandler { public HttpRequestHandler(NettyHttpServerTransport serverTransport, boolean detailedErrorsEnabled, ThreadContext threadContext) { this.serverTransport = serverTransport; - this.corsPattern = RestUtils.checkCorsSettingForRegex(serverTransport.settings().get(NettyHttpServerTransport.SETTING_CORS_ALLOW_ORIGIN)); + this.corsPattern = RestUtils + .checkCorsSettingForRegex(HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN.get(serverTransport.settings())); this.httpPipeliningEnabled = serverTransport.pipelining; this.detailedErrorsEnabled = detailedErrorsEnabled; this.threadContext = threadContext; diff --git a/core/src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java b/core/src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java index 316799dd062..1d3a2966e34 100644 --- a/core/src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java +++ b/core/src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java @@ -49,12 +49,12 @@ import java.util.Map; import java.util.Set; import java.util.regex.Pattern; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_CREDENTIALS; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_HEADERS; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_METHODS; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_ORIGIN; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ENABLED; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_MAX_AGE; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_HEADERS; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_MAX_AGE; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_METHODS; @@ -117,7 +117,7 @@ public class NettyHttpChannel extends HttpChannel { String originHeader = request.header(ORIGIN); if (!Strings.isNullOrEmpty(originHeader)) { if (corsPattern == null) { - String allowedOrigins = transport.settings().get(SETTING_CORS_ALLOW_ORIGIN, null); + String allowedOrigins = SETTING_CORS_ALLOW_ORIGIN.get(transport.settings()); if (!Strings.isNullOrEmpty(allowedOrigins)) { resp.headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins); } @@ -128,8 +128,8 @@ public class NettyHttpChannel extends HttpChannel { if (nettyRequest.getMethod() == HttpMethod.OPTIONS) { // Allow Ajax requests based on the CORS "preflight" request resp.headers().add(ACCESS_CONTROL_MAX_AGE, SETTING_CORS_MAX_AGE.get(transport.settings())); - resp.headers().add(ACCESS_CONTROL_ALLOW_METHODS, transport.settings().get(SETTING_CORS_ALLOW_METHODS, "OPTIONS, HEAD, GET, POST, PUT, DELETE")); - resp.headers().add(ACCESS_CONTROL_ALLOW_HEADERS, transport.settings().get(SETTING_CORS_ALLOW_HEADERS, "X-Requested-With, Content-Type, Content-Length")); + resp.headers().add(ACCESS_CONTROL_ALLOW_METHODS, SETTING_CORS_ALLOW_METHODS.get(transport.settings())); + resp.headers().add(ACCESS_CONTROL_ALLOW_HEADERS, SETTING_CORS_ALLOW_HEADERS.get(transport.settings())); } if (SETTING_CORS_ALLOW_CREDENTIALS.get(transport.settings())) { diff --git a/core/src/main/java/org/elasticsearch/http/netty/NettyHttpServerTransport.java b/core/src/main/java/org/elasticsearch/http/netty/NettyHttpServerTransport.java index 00b3c0f8afa..83e6823f6f0 100644 --- a/core/src/main/java/org/elasticsearch/http/netty/NettyHttpServerTransport.java +++ b/core/src/main/java/org/elasticsearch/http/netty/NettyHttpServerTransport.java @@ -26,8 +26,6 @@ import org.elasticsearch.common.netty.NettyUtils; import org.elasticsearch.common.netty.OpenChannelsHandler; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.network.NetworkService; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Setting.Scope; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.InetSocketTransportAddress; @@ -46,6 +44,7 @@ import org.elasticsearch.http.HttpRequest; import org.elasticsearch.http.HttpServerAdapter; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.http.HttpStats; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.http.netty.pipelining.HttpPipeliningHandler; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.threadpool.ThreadPool; @@ -75,7 +74,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; - import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_BLOCKING; import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_KEEP_ALIVE; import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_NO_DELAY; @@ -93,22 +91,6 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent SETTING_CORS_ENABLED = Setting.boolSetting("http.cors.enabled", false, false, Scope.CLUSTER); - public static final String SETTING_CORS_ALLOW_ORIGIN = "http.cors.allow-origin"; - public static final Setting SETTING_CORS_MAX_AGE = Setting.intSetting("http.cors.max-age", 1728000, false, Scope.CLUSTER); - public static final String SETTING_CORS_ALLOW_METHODS = "http.cors.allow-methods"; - public static final String SETTING_CORS_ALLOW_HEADERS = "http.cors.allow-headers"; - public static final Setting SETTING_CORS_ALLOW_CREDENTIALS = Setting.boolSetting("http.cors.allow-credentials", false, false, Scope.CLUSTER); - - public static final Setting SETTING_PIPELINING = Setting.boolSetting("http.pipelining", true, false, Scope.CLUSTER); - public static final String SETTING_PIPELINING_MAX_EVENTS = "http.pipelining.max_events"; - public static final String SETTING_HTTP_COMPRESSION = "http.compression"; - public static final String SETTING_HTTP_COMPRESSION_LEVEL = "http.compression_level"; - public static final Setting SETTING_HTTP_DETAILED_ERRORS_ENABLED = Setting.boolSetting("http.detailed_errors.enabled", true, false, Scope.CLUSTER); - - public static final int DEFAULT_SETTING_PIPELINING_MAX_EVENTS = 10000; - public static final String DEFAULT_PORT_RANGE = "9200-9300"; - protected final NetworkService networkService; protected final BigArrays bigArrays; @@ -131,7 +113,7 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent 0) { @@ -215,10 +194,10 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent Integer.MAX_VALUE) { @@ -312,10 +291,9 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent lastException = new AtomicReference<>(); final AtomicReference boundSocket = new AtomicReference<>(); - boolean success = portsRange.iterate(new PortsRange.PortCallback() { + boolean success = port.iterate(new PortsRange.PortCallback() { @Override public boolean onPortNumber(int portNumber) { try { diff --git a/core/src/main/java/org/elasticsearch/rest/support/RestUtils.java b/core/src/main/java/org/elasticsearch/rest/support/RestUtils.java index 56bb18d5e6e..167e858c1df 100644 --- a/core/src/main/java/org/elasticsearch/rest/support/RestUtils.java +++ b/core/src/main/java/org/elasticsearch/rest/support/RestUtils.java @@ -57,7 +57,7 @@ public class RestUtils { if (fromIndex >= s.length()) { return; } - + int queryStringLength = s.contains("#") ? s.indexOf("#") : s.length(); String name = null; diff --git a/core/src/test/java/org/elasticsearch/http/netty/HttpPublishPortIT.java b/core/src/test/java/org/elasticsearch/http/netty/HttpPublishPortIT.java index f227a9a03b4..b6cf9d91894 100644 --- a/core/src/test/java/org/elasticsearch/http/netty/HttpPublishPortIT.java +++ b/core/src/test/java/org/elasticsearch/http/netty/HttpPublishPortIT.java @@ -24,7 +24,7 @@ import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.InetSocketTransportAddress; -import org.elasticsearch.node.Node; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; @@ -41,7 +41,7 @@ public class HttpPublishPortIT extends ESIntegTestCase { return Settings.settingsBuilder() .put(super.nodeSettings(nodeOrdinal)) .put(NetworkModule.HTTP_ENABLED.getKey(), true) - .put("http.publish_port", 9080) + .put(HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT.getKey(), 9080) .build(); } diff --git a/core/src/test/java/org/elasticsearch/http/netty/NettyHttpChannelTests.java b/core/src/test/java/org/elasticsearch/http/netty/NettyHttpChannelTests.java index 017eef345a7..6311e56834d 100644 --- a/core/src/test/java/org/elasticsearch/http/netty/NettyHttpChannelTests.java +++ b/core/src/test/java/org/elasticsearch/http/netty/NettyHttpChannelTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.MockBigArrays; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; @@ -81,7 +82,7 @@ public class NettyHttpChannelTests extends ESTestCase { public void testCorsEnabledWithoutAllowOrigins() { // Set up a HTTP transport with only the CORS enabled setting Settings settings = Settings.builder() - .put(NettyHttpServerTransport.SETTING_CORS_ENABLED.getKey(), true) + .put(HttpTransportSettings.SETTING_CORS_ENABLED.getKey(), true) .build(); httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool); HttpRequest httpRequest = new TestHttpRequest(); @@ -104,8 +105,8 @@ public class NettyHttpChannelTests extends ESTestCase { public void testCorsEnabledWithAllowOrigins() { // create a http transport with CORS enabled and allow origin configured Settings settings = Settings.builder() - .put(NettyHttpServerTransport.SETTING_CORS_ENABLED.getKey(), true) - .put(NettyHttpServerTransport.SETTING_CORS_ALLOW_ORIGIN, "remote-host") + .put(HttpTransportSettings.SETTING_CORS_ENABLED.getKey(), true) + .put(HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN.getKey(), "remote-host") .build(); httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool); HttpRequest httpRequest = new TestHttpRequest(); diff --git a/core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java b/core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java index 2a121be509c..d1f25a8fb4f 100644 --- a/core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java +++ b/core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java @@ -23,8 +23,7 @@ import org.apache.http.impl.client.HttpClients; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.http.netty.NettyHttpServerTransport; -import org.elasticsearch.node.Node; +import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; @@ -45,7 +44,7 @@ public class DetailedErrorsDisabledIT extends ESIntegTestCase { return Settings.settingsBuilder() .put(super.nodeSettings(nodeOrdinal)) .put(NetworkModule.HTTP_ENABLED.getKey(), true) - .put(NettyHttpServerTransport.SETTING_HTTP_DETAILED_ERRORS_ENABLED.getKey(), false) + .put(HttpTransportSettings.SETTING_HTTP_DETAILED_ERRORS_ENABLED.getKey(), false) .build(); } diff --git a/core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java b/core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java index 1c624f98e2f..9740032ed7e 100644 --- a/core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java +++ b/core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java @@ -22,15 +22,14 @@ import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.node.Node; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; import org.elasticsearch.test.rest.client.http.HttpResponse; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_CREDENTIALS; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ALLOW_ORIGIN; -import static org.elasticsearch.http.netty.NettyHttpServerTransport.SETTING_CORS_ENABLED; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -47,7 +46,7 @@ public class CorsRegexIT extends ESIntegTestCase { protected Settings nodeSettings(int nodeOrdinal) { return Settings.settingsBuilder() .put(super.nodeSettings(nodeOrdinal)) - .put(SETTING_CORS_ALLOW_ORIGIN, "/https?:\\/\\/localhost(:[0-9]+)?/") + .put(SETTING_CORS_ALLOW_ORIGIN.getKey(), "/https?:\\/\\/localhost(:[0-9]+)?/") .put(SETTING_CORS_ALLOW_CREDENTIALS.getKey(), true) .put(SETTING_CORS_ENABLED.getKey(), true) .put(NetworkModule.HTTP_ENABLED.getKey(), true) From ed7bc5ba16d8746de5f709508db6d3d3c60ef574 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 1 Feb 2016 07:33:44 -0500 Subject: [PATCH 08/49] Unused variables in TransportReplicationAction --- .../action/support/replication/TransportReplicationAction.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index b5a37124b8b..c40d3fb579a 100644 --- a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -320,7 +320,6 @@ public abstract class TransportReplicationAction Date: Thu, 28 Jan 2016 14:53:32 +0100 Subject: [PATCH 09/49] Migrate Azure settings to new settings infrastructure With this commit we migrate all Azure related settings to the new settings infrastructure. --- .../cloud/azure/AzureDiscoveryModule.java | 34 +++++----- .../azure/management/AzureComputeService.java | 25 +++---- .../management/AzureComputeServiceImpl.java | 23 ++----- .../AzureComputeSettingsFilter.java | 16 ++--- .../AbstractAzureComputeServiceTestCase.java | 8 +-- .../discovery/azure/AzureSimpleTests.java | 6 +- .../azure/AzureTwoStartedNodesTests.java | 4 +- .../cloud/azure/blobstore/AzureBlobStore.java | 18 +++-- .../azure/storage/AzureStorageService.java | 20 +++--- .../storage/AzureStorageServiceImpl.java | 9 +-- .../azure/storage/AzureStorageSettings.java | 50 +++++++------- .../storage/AzureStorageSettingsFilter.java | 2 +- .../repositories/azure/AzureRepository.java | 65 ++++++++----------- ...bstractAzureRepositoryServiceTestCase.java | 2 +- ...est.java => AzureStorageServiceTests.java} | 12 ++-- ...a => AzureStorageSettingsFilterTests.java} | 2 +- ...est.java => AzureSettingsParserTests.java} | 2 +- .../azure/AzureSnapshotRestoreTests.java | 46 ++++++------- 18 files changed, 155 insertions(+), 189 deletions(-) rename plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/{AzureStorageServiceTest.java => AzureStorageServiceTests.java} (93%) rename plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/{AzureStorageSettingsFilterTest.java => AzureStorageSettingsFilterTests.java} (97%) rename plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/{AzureSettingsParserTest.java => AzureSettingsParserTests.java} (98%) diff --git a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/AzureDiscoveryModule.java b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/AzureDiscoveryModule.java index d48eed9e507..5a9b9f5f412 100644 --- a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/AzureDiscoveryModule.java +++ b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/AzureDiscoveryModule.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.DiscoveryModule; import org.elasticsearch.discovery.azure.AzureDiscovery; @@ -79,29 +80,24 @@ public class AzureDiscoveryModule extends AbstractModule { return false; } - if (isPropertyMissing(settings, Management.SUBSCRIPTION_ID) || - isPropertyMissing(settings, Management.SERVICE_NAME) || - isPropertyMissing(settings, Management.KEYSTORE_PATH) || - isPropertyMissing(settings, Management.KEYSTORE_PASSWORD) - ) { - logger.debug("one or more azure discovery settings are missing. " + + if (isDefined(settings, Management.SUBSCRIPTION_ID_SETTING) && + isDefined(settings, Management.SERVICE_NAME_SETTING) && + isDefined(settings, Management.KEYSTORE_PATH_SETTING) && + isDefined(settings, Management.KEYSTORE_PASSWORD_SETTING)) { + logger.trace("All required properties for Azure discovery are set!"); + return true; + } else { + logger.debug("One or more Azure discovery settings are missing. " + "Check elasticsearch.yml file. Should have [{}], [{}], [{}] and [{}].", - Management.SUBSCRIPTION_ID, - Management.SERVICE_NAME, - Management.KEYSTORE_PATH, - Management.KEYSTORE_PASSWORD); + Management.SUBSCRIPTION_ID_SETTING.getKey(), + Management.SERVICE_NAME_SETTING.getKey(), + Management.KEYSTORE_PATH_SETTING.getKey(), + Management.KEYSTORE_PASSWORD_SETTING.getKey()); return false; } - - logger.trace("all required properties for azure discovery are set!"); - - return true; } - public static boolean isPropertyMissing(Settings settings, String name) throws ElasticsearchException { - if (!Strings.hasText(settings.get(name))) { - return true; - } - return false; + private static boolean isDefined(Settings settings, Setting property) throws ElasticsearchException { + return (property.exists(settings) && Strings.hasText(property.get(settings))); } } diff --git a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeService.java b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeService.java index de2343d9d87..0c665c138b8 100644 --- a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeService.java +++ b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeService.java @@ -19,31 +19,25 @@ package org.elasticsearch.cloud.azure.management; +import com.microsoft.windowsazure.core.utils.KeyStoreType; import com.microsoft.windowsazure.management.compute.models.HostedServiceGetDetailedResponse; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.discovery.azure.AzureUnicastHostsProvider; -import java.util.Locale; - -/** - * - */ public interface AzureComputeService { - static public final class Management { - public static final String API_IMPLEMENTATION = "cloud.azure.management.api.impl"; - - public static final String SUBSCRIPTION_ID = "cloud.azure.management.subscription.id"; - public static final String SERVICE_NAME = "cloud.azure.management.cloud.service.name"; + final class Management { + public static final Setting SUBSCRIPTION_ID_SETTING = Setting.simpleString("cloud.azure.management.subscription.id", false, Setting.Scope.CLUSTER); + public static final Setting SERVICE_NAME_SETTING = Setting.simpleString("cloud.azure.management.cloud.service.name", false, Setting.Scope.CLUSTER); // Keystore settings - public static final String KEYSTORE_PATH = "cloud.azure.management.keystore.path"; - public static final String KEYSTORE_PASSWORD = "cloud.azure.management.keystore.password"; - public static final String KEYSTORE_TYPE = "cloud.azure.management.keystore.type"; + public static final Setting KEYSTORE_PATH_SETTING = Setting.simpleString("cloud.azure.management.keystore.path", false, Setting.Scope.CLUSTER); + public static final Setting KEYSTORE_PASSWORD_SETTING = Setting.simpleString("cloud.azure.management.keystore.password", false, Setting.Scope.CLUSTER); + public static final Setting KEYSTORE_TYPE_SETTING = new Setting<>("cloud.azure.management.keystore.type", KeyStoreType.pkcs12.name(), KeyStoreType::fromString, false, Setting.Scope.CLUSTER); } - static public final class Discovery { + final class Discovery { public static final Setting REFRESH_SETTING = Setting.positiveTimeSetting("discovery.azure.refresh_interval", TimeValue.timeValueSeconds(0), false, Setting.Scope.CLUSTER); public static final Setting HOST_TYPE_SETTING = new Setting<>("discovery.azure.host.type", @@ -53,5 +47,6 @@ public interface AzureComputeService { public static final String DEPLOYMENT_NAME = "discovery.azure.deployment.name"; public static final String DEPLOYMENT_SLOT = "discovery.azure.deployment.slot"; } - public HostedServiceGetDetailedResponse getServiceDetails(); + + HostedServiceGetDetailedResponse getServiceDetails(); } diff --git a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeServiceImpl.java b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeServiceImpl.java index 39221ee6904..04b4f32ea92 100644 --- a/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeServiceImpl.java +++ b/plugins/discovery-azure/src/main/java/org/elasticsearch/cloud/azure/management/AzureComputeServiceImpl.java @@ -36,11 +36,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import static org.elasticsearch.cloud.azure.management.AzureComputeService.Management.KEYSTORE_PASSWORD; -import static org.elasticsearch.cloud.azure.management.AzureComputeService.Management.KEYSTORE_PATH; -import static org.elasticsearch.cloud.azure.management.AzureComputeService.Management.KEYSTORE_TYPE; -import static org.elasticsearch.cloud.azure.management.AzureComputeService.Management.SUBSCRIPTION_ID; - /** * */ @@ -57,20 +52,12 @@ public class AzureComputeServiceImpl extends AbstractLifecycleComponent start one node"); @@ -53,7 +53,7 @@ public class AzureSimpleTests extends AbstractAzureComputeServiceTestCase { public void testOneNodeShouldRunUsingPublicIp() { Settings.Builder settings = Settings.settingsBuilder() - .put(Management.SERVICE_NAME, "dummy") + .put(Management.SERVICE_NAME_SETTING.getKey(), "dummy") .put(Discovery.HOST_TYPE_SETTING.getKey(), "public_ip"); logger.info("--> start one node"); @@ -66,7 +66,7 @@ public class AzureSimpleTests extends AbstractAzureComputeServiceTestCase { public void testOneNodeShouldRunUsingWrongSettings() { Settings.Builder settings = Settings.settingsBuilder() - .put(Management.SERVICE_NAME, "dummy") + .put(Management.SERVICE_NAME_SETTING.getKey(), "dummy") .put(Discovery.HOST_TYPE_SETTING.getKey(), "do_not_exist"); logger.info("--> start one node"); diff --git a/plugins/discovery-azure/src/test/java/org/elasticsearch/discovery/azure/AzureTwoStartedNodesTests.java b/plugins/discovery-azure/src/test/java/org/elasticsearch/discovery/azure/AzureTwoStartedNodesTests.java index 880c05ed121..bb15ad050f0 100644 --- a/plugins/discovery-azure/src/test/java/org/elasticsearch/discovery/azure/AzureTwoStartedNodesTests.java +++ b/plugins/discovery-azure/src/test/java/org/elasticsearch/discovery/azure/AzureTwoStartedNodesTests.java @@ -41,7 +41,7 @@ public class AzureTwoStartedNodesTests extends AbstractAzureComputeServiceTestCa @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/11533") public void testTwoNodesShouldRunUsingPrivateIp() { Settings.Builder settings = Settings.settingsBuilder() - .put(Management.SERVICE_NAME, "dummy") + .put(Management.SERVICE_NAME_SETTING.getKey(), "dummy") .put(Discovery.HOST_TYPE_SETTING.getKey(), "private_ip"); logger.info("--> start first node"); @@ -59,7 +59,7 @@ public class AzureTwoStartedNodesTests extends AbstractAzureComputeServiceTestCa @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/11533") public void testTwoNodesShouldRunUsingPublicIp() { Settings.Builder settings = Settings.settingsBuilder() - .put(Management.SERVICE_NAME, "dummy") + .put(Management.SERVICE_NAME_SETTING.getKey(), "dummy") .put(Discovery.HOST_TYPE_SETTING.getKey(), "public_ip"); logger.info("--> start first node"); diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/blobstore/AzureBlobStore.java b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/blobstore/AzureBlobStore.java index cf97008249f..85baf00b909 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/blobstore/AzureBlobStore.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/blobstore/AzureBlobStore.java @@ -23,6 +23,7 @@ import com.microsoft.azure.storage.LocationMode; import com.microsoft.azure.storage.StorageException; import org.elasticsearch.cloud.azure.storage.AzureStorageService; import org.elasticsearch.cloud.azure.storage.AzureStorageService.Storage; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobMetaData; import org.elasticsearch.common.blobstore.BlobPath; @@ -32,7 +33,6 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.RepositoryName; import org.elasticsearch.repositories.RepositorySettings; -import org.elasticsearch.repositories.azure.AzureRepository.Defaults; import java.io.InputStream; import java.io.OutputStream; @@ -40,7 +40,7 @@ import java.net.URISyntaxException; import java.util.Locale; import java.util.Map; -import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getRepositorySettings; +import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getValue; import static org.elasticsearch.repositories.azure.AzureRepository.Repository; public class AzureBlobStore extends AbstractComponent implements BlobStore { @@ -57,17 +57,15 @@ public class AzureBlobStore extends AbstractComponent implements BlobStore { AzureStorageService client) throws URISyntaxException, StorageException { super(settings); this.client = client.start(); - this.container = getRepositorySettings(repositorySettings, Repository.CONTAINER, Storage.CONTAINER, Defaults.CONTAINER); + this.container = getValue(repositorySettings, Repository.CONTAINER_SETTING, Storage.CONTAINER_SETTING); this.repositoryName = name.getName(); + this.accountName = getValue(repositorySettings, Repository.ACCOUNT_SETTING, Storage.ACCOUNT_SETTING); - // NOTE: null account means to use the first one specified in config - this.accountName = getRepositorySettings(repositorySettings, Repository.ACCOUNT, Storage.ACCOUNT, null); - - String modeStr = getRepositorySettings(repositorySettings, Repository.LOCATION_MODE, Storage.LOCATION_MODE, null); - if (modeStr == null) { - this.locMode = LocationMode.PRIMARY_ONLY; - } else { + String modeStr = getValue(repositorySettings, Repository.LOCATION_MODE_SETTING, Storage.LOCATION_MODE_SETTING); + if (Strings.hasLength(modeStr)) { this.locMode = LocationMode.valueOf(modeStr.toUpperCase(Locale.ROOT)); + } else { + this.locMode = LocationMode.PRIMARY_ONLY; } } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageService.java b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageService.java index c154f78eeb5..657c292db31 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageService.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageService.java @@ -23,11 +23,15 @@ import com.microsoft.azure.storage.LocationMode; import com.microsoft.azure.storage.StorageException; import org.elasticsearch.common.blobstore.BlobMetaData; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; import java.io.InputStream; import java.io.OutputStream; import java.net.URISyntaxException; import java.util.Map; +import java.util.function.Function; /** * Azure Storage Service interface @@ -37,15 +41,13 @@ public interface AzureStorageService { final class Storage { public static final String PREFIX = "cloud.azure.storage."; - - public static final String TIMEOUT = "cloud.azure.storage.timeout"; - - public static final String ACCOUNT = "repositories.azure.account"; - public static final String LOCATION_MODE = "repositories.azure.location_mode"; - public static final String CONTAINER = "repositories.azure.container"; - public static final String BASE_PATH = "repositories.azure.base_path"; - public static final String CHUNK_SIZE = "repositories.azure.chunk_size"; - public static final String COMPRESS = "repositories.azure.compress"; + public static final Setting TIMEOUT_SETTING = Setting.timeSetting("cloud.azure.storage.timeout", TimeValue.timeValueMinutes(5), false, Setting.Scope.CLUSTER); + public static final Setting ACCOUNT_SETTING = Setting.simpleString("repositories.azure.account", false, Setting.Scope.CLUSTER); + public static final Setting CONTAINER_SETTING = Setting.simpleString("repositories.azure.container", false, Setting.Scope.CLUSTER); + public static final Setting BASE_PATH_SETTING = Setting.simpleString("repositories.azure.base_path", false, Setting.Scope.CLUSTER); + public static final Setting LOCATION_MODE_SETTING = Setting.simpleString("repositories.azure.location_mode", false, Setting.Scope.CLUSTER); + public static final Setting CHUNK_SIZE_SETTING = Setting.byteSizeSetting("repositories.azure.chunk_size", new ByteSizeValue(-1), false, Setting.Scope.CLUSTER); + public static final Setting COMPRESS_SETTING = Setting.boolSetting("repositories.azure.compress", false, false, Setting.Scope.CLUSTER); } boolean doesContainerExist(String account, LocationMode mode, String container); diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceImpl.java b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceImpl.java index 3cecd810a95..74ba008c8ec 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceImpl.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceImpl.java @@ -28,6 +28,7 @@ import com.microsoft.azure.storage.blob.CloudBlobContainer; import com.microsoft.azure.storage.blob.CloudBlockBlob; import com.microsoft.azure.storage.blob.ListBlobItem; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobMetaData; import org.elasticsearch.common.blobstore.support.PlainBlobMetaData; import org.elasticsearch.common.collect.MapBuilder; @@ -41,7 +42,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; -import java.util.Hashtable; +import java.util.HashMap; import java.util.Map; public class AzureStorageServiceImpl extends AbstractLifecycleComponent @@ -60,7 +61,7 @@ public class AzureStorageServiceImpl extends AbstractLifecycleComponent(); + this.clients = new HashMap<>(); } void createClient(AzureStorageSettings azureStorageSettings) { @@ -94,13 +95,13 @@ public class AzureStorageServiceImpl extends AbstractLifecycleComponent secondaryStorage = new HashMap<>(); - TimeValue globalTimeout = settings.getAsTime(Storage.TIMEOUT, TimeValue.timeValueMinutes(5)); + TimeValue globalTimeout = Storage.TIMEOUT_SETTING.get(settings); Settings storageSettings = settings.getByPrefix(Storage.PREFIX); if (storageSettings != null) { @@ -124,27 +124,23 @@ public class AzureStorageSettings { return Tuple.tuple(primaryStorage, secondaryStorage); } - public static String getRepositorySettings(RepositorySettings repositorySettings, - String repositorySettingName, - String repositoriesSettingName, - String defaultValue) { - return repositorySettings.settings().get(repositorySettingName, - repositorySettings.globalSettings().get(repositoriesSettingName, defaultValue)); + public static T getValue(RepositorySettings repositorySettings, + Setting repositorySetting, + Setting repositoriesSetting) { + if (repositorySetting.exists(repositorySettings.settings())) { + return repositorySetting.get(repositorySettings.settings()); + } else { + return repositoriesSetting.get(repositorySettings.globalSettings()); + } } - public static ByteSizeValue getRepositorySettingsAsBytesSize(RepositorySettings repositorySettings, - String repositorySettingName, - String repositoriesSettingName, - ByteSizeValue defaultValue) { - return repositorySettings.settings().getAsBytesSize(repositorySettingName, - repositorySettings.globalSettings().getAsBytesSize(repositoriesSettingName, defaultValue)); - } - - public static Boolean getRepositorySettingsAsBoolean(RepositorySettings repositorySettings, - String repositorySettingName, - String repositoriesSettingName, - Boolean defaultValue) { - return repositorySettings.settings().getAsBoolean(repositorySettingName, - repositorySettings.globalSettings().getAsBoolean(repositoriesSettingName, defaultValue)); + public static Setting getEffectiveSetting(RepositorySettings repositorySettings, + Setting repositorySetting, + Setting repositoriesSetting) { + if (repositorySetting.exists(repositorySettings.settings())) { + return repositorySetting; + } else { + return repositoriesSetting; + } } } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilter.java b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilter.java index 2c4e7957af3..76ac68bd436 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilter.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilter.java @@ -33,6 +33,6 @@ public class AzureStorageSettingsFilter extends AbstractComponent { // Cloud storage API settings needed to be hidden settingsFilter.addFilter(Storage.PREFIX + "*.account"); settingsFilter.addFilter(Storage.PREFIX + "*.key"); - settingsFilter.addFilter(Storage.ACCOUNT); + settingsFilter.addFilter(Storage.ACCOUNT_SETTING.getKey()); } } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java index a3abf9b4adf..f2773bccbbd 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java @@ -29,6 +29,8 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.index.snapshots.IndexShardRepository; @@ -42,10 +44,10 @@ import java.io.IOException; import java.net.URISyntaxException; import java.util.List; import java.util.Locale; +import java.util.function.Function; -import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getRepositorySettings; -import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getRepositorySettingsAsBoolean; -import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getRepositorySettingsAsBytesSize; +import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getEffectiveSetting; +import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getValue; /** * Azure file system implementation of the BlobStoreRepository @@ -60,31 +62,23 @@ import static org.elasticsearch.cloud.azure.storage.AzureStorageSettings.getRepo */ public class AzureRepository extends BlobStoreRepository { + private static final ByteSizeValue MAX_CHUNK_SIZE = new ByteSizeValue(64, ByteSizeUnit.MB); + public final static String TYPE = "azure"; - static public final class Defaults { - public static final String CONTAINER = "elasticsearch-snapshots"; - public static final ByteSizeValue CHUNK_SIZE = new ByteSizeValue(64, ByteSizeUnit.MB); - public static final Boolean COMPRESS = false; - } - - - static public final class Repository { - public static final String ACCOUNT = "account"; - public static final String LOCATION_MODE = "location_mode"; - public static final String CONTAINER = "container"; - public static final String CHUNK_SIZE = "chunk_size"; - public static final String COMPRESS = "compress"; - public static final String BASE_PATH = "base_path"; + public static final class Repository { + public static final Setting ACCOUNT_SETTING = Setting.simpleString("account", false, Setting.Scope.CLUSTER); + public static final Setting CONTAINER_SETTING = new Setting<>("container", "elasticsearch-snapshots", Function.identity(), false, Setting.Scope.CLUSTER); + public static final Setting BASE_PATH_SETTING = Setting.simpleString("base_path", false, Setting.Scope.CLUSTER); + public static final Setting LOCATION_MODE_SETTING = Setting.simpleString("location_mode", false, Setting.Scope.CLUSTER); + public static final Setting CHUNK_SIZE_SETTING = Setting.byteSizeSetting("chunk_size", MAX_CHUNK_SIZE, false, Setting.Scope.CLUSTER); + public static final Setting COMPRESS_SETTING = Setting.boolSetting("compress", false, false, Setting.Scope.CLUSTER); } private final AzureBlobStore blobStore; - private final BlobPath basePath; - - private ByteSizeValue chunkSize; - - private boolean compress; + private final ByteSizeValue chunkSize; + private final boolean compress; private final boolean readonly; @Inject @@ -93,30 +87,27 @@ public class AzureRepository extends BlobStoreRepository { AzureBlobStore azureBlobStore) throws IOException, URISyntaxException, StorageException { super(name.getName(), repositorySettings, indexShardRepository); - String container = getRepositorySettings(repositorySettings, Repository.CONTAINER, Storage.CONTAINER, Defaults.CONTAINER); + String container = getValue(repositorySettings, Repository.CONTAINER_SETTING, Storage.CONTAINER_SETTING); this.blobStore = azureBlobStore; - this.chunkSize = getRepositorySettingsAsBytesSize(repositorySettings, Repository.CHUNK_SIZE, Storage.CHUNK_SIZE, Defaults.CHUNK_SIZE); - - if (this.chunkSize.getMb() > 64) { - logger.warn("azure repository does not support yet size > 64mb. Fall back to 64mb."); - this.chunkSize = new ByteSizeValue(64, ByteSizeUnit.MB); + ByteSizeValue configuredChunkSize = getValue(repositorySettings, Repository.CHUNK_SIZE_SETTING, Storage.CHUNK_SIZE_SETTING); + if (configuredChunkSize.getMb() > MAX_CHUNK_SIZE.getMb()) { + Setting setting = getEffectiveSetting(repositorySettings, Repository.CHUNK_SIZE_SETTING, Storage.CHUNK_SIZE_SETTING); + throw new SettingsException("[" + setting.getKey() + "] must not exceed [" + MAX_CHUNK_SIZE + "] but is set to [" + configuredChunkSize + "]."); + } else { + this.chunkSize = configuredChunkSize; } - this.compress = getRepositorySettingsAsBoolean(repositorySettings, Repository.COMPRESS, Storage.COMPRESS, Defaults.COMPRESS); - String modeStr = getRepositorySettings(repositorySettings, Repository.LOCATION_MODE, Storage.LOCATION_MODE, null); - if (modeStr != null) { + this.compress = getValue(repositorySettings, Repository.COMPRESS_SETTING, Storage.COMPRESS_SETTING); + String modeStr = getValue(repositorySettings, Repository.LOCATION_MODE_SETTING, Storage.LOCATION_MODE_SETTING); + if (Strings.hasLength(modeStr)) { LocationMode locationMode = LocationMode.valueOf(modeStr.toUpperCase(Locale.ROOT)); - if (locationMode == LocationMode.SECONDARY_ONLY) { - readonly = true; - } else { - readonly = false; - } + readonly = locationMode == LocationMode.SECONDARY_ONLY; } else { readonly = false; } - String basePath = getRepositorySettings(repositorySettings, Repository.BASE_PATH, Storage.BASE_PATH, null); + String basePath = getValue(repositorySettings, Repository.BASE_PATH_SETTING, Storage.BASE_PATH_SETTING); if (Strings.hasLength(basePath)) { // Remove starting / if any diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/AbstractAzureRepositoryServiceTestCase.java b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/AbstractAzureRepositoryServiceTestCase.java index 73aa7e3921c..d8bca609ce3 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/AbstractAzureRepositoryServiceTestCase.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/AbstractAzureRepositoryServiceTestCase.java @@ -80,7 +80,7 @@ public abstract class AbstractAzureRepositoryServiceTestCase extends AbstractAzu @Override protected Settings nodeSettings(int nodeOrdinal) { Settings.Builder builder = Settings.settingsBuilder() - .put(Storage.CONTAINER, "snapshots"); + .put(Storage.CONTAINER_SETTING.getKey(), "snapshots"); return builder.build(); } diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTest.java b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTests.java similarity index 93% rename from plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTest.java rename to plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTests.java index 93683d9d014..5fc4937ea92 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTest.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageServiceTests.java @@ -28,7 +28,7 @@ import java.net.URI; import static org.hamcrest.Matchers.is; -public class AzureStorageServiceTest extends ESTestCase { +public class AzureStorageServiceTests extends ESTestCase { final static Settings settings = Settings.builder() .put("cloud.azure.storage.azure1.account", "myaccount1") .put("cloud.azure.storage.azure1.key", "mykey1") @@ -120,24 +120,24 @@ public class AzureStorageServiceTest extends ESTestCase { public void testGetSelectedClientGlobalTimeout() { Settings timeoutSettings = Settings.builder() .put(settings) - .put("cloud.azure.storage.timeout", "10s") + .put(AzureStorageService.Storage.TIMEOUT_SETTING.getKey(), "10s") .build(); AzureStorageServiceImpl azureStorageService = new AzureStorageServiceMock(timeoutSettings); azureStorageService.doStart(); CloudBlobClient client1 = azureStorageService.getSelectedClient("azure1", LocationMode.PRIMARY_ONLY); - assertThat(client1.getDefaultRequestOptions().getTimeoutIntervalInMs(), is(10 * 1000)); + assertThat(client1.getDefaultRequestOptions().getMaximumExecutionTimeInMs(), is(10 * 1000)); CloudBlobClient client3 = azureStorageService.getSelectedClient("azure3", LocationMode.PRIMARY_ONLY); - assertThat(client3.getDefaultRequestOptions().getTimeoutIntervalInMs(), is(30 * 1000)); + assertThat(client3.getDefaultRequestOptions().getMaximumExecutionTimeInMs(), is(30 * 1000)); } public void testGetSelectedClientDefaultTimeout() { AzureStorageServiceImpl azureStorageService = new AzureStorageServiceMock(settings); azureStorageService.doStart(); CloudBlobClient client1 = azureStorageService.getSelectedClient("azure1", LocationMode.PRIMARY_ONLY); - assertThat(client1.getDefaultRequestOptions().getTimeoutIntervalInMs(), is(5 * 60 * 1000)); + assertThat(client1.getDefaultRequestOptions().getMaximumExecutionTimeInMs(), is(5 * 60 * 1000)); CloudBlobClient client3 = azureStorageService.getSelectedClient("azure3", LocationMode.PRIMARY_ONLY); - assertThat(client3.getDefaultRequestOptions().getTimeoutIntervalInMs(), is(30 * 1000)); + assertThat(client3.getDefaultRequestOptions().getMaximumExecutionTimeInMs(), is(30 * 1000)); } /** diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTest.java b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTests.java similarity index 97% rename from plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTest.java rename to plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTests.java index eaaf9c224d8..6e36b27cebe 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTest.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/cloud/azure/storage/AzureStorageSettingsFilterTests.java @@ -31,7 +31,7 @@ import java.io.IOException; import static org.hamcrest.Matchers.contains; -public class AzureStorageSettingsFilterTest extends ESTestCase { +public class AzureStorageSettingsFilterTests extends ESTestCase { final static Settings settings = Settings.builder() .put("cloud.azure.storage.azure1.account", "myaccount1") .put("cloud.azure.storage.azure1.key", "mykey1") diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTest.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTests.java similarity index 98% rename from plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTest.java rename to plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTests.java index aec8506ca6d..5347be09da0 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTest.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTests.java @@ -31,7 +31,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -public class AzureSettingsParserTest extends LuceneTestCase { +public class AzureSettingsParserTests extends LuceneTestCase { public void testParseTwoSettingsExplicitDefault() { Settings settings = Settings.builder() diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSnapshotRestoreTests.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSnapshotRestoreTests.java index 1818a5e6252..7f6f3106fc3 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSnapshotRestoreTests.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSnapshotRestoreTests.java @@ -106,9 +106,9 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository with path [{}]", getRepositoryPath()); PutRepositoryResponse putRepositoryResponse = client.admin().cluster().preparePutRepository("test-repo") .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.CHUNK_SIZE, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.CHUNK_SIZE_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -197,9 +197,9 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("creating Azure repository with path [{}]", getRepositoryPath()); PutRepositoryResponse putRepositoryResponse = client.admin().cluster().preparePutRepository(repositoryName) .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.BASE_PATH, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.BASE_PATH_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -237,16 +237,16 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository with path [{}]", getRepositoryPath()); PutRepositoryResponse putRepositoryResponse1 = client.admin().cluster().preparePutRepository("test-repo1") .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName().concat("-1")) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.CHUNK_SIZE, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName().concat("-1")) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.CHUNK_SIZE_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); assertThat(putRepositoryResponse1.isAcknowledged(), equalTo(true)); PutRepositoryResponse putRepositoryResponse2 = client.admin().cluster().preparePutRepository("test-repo2") .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName().concat("-2")) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.CHUNK_SIZE, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName().concat("-2")) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.CHUNK_SIZE_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); assertThat(putRepositoryResponse2.isAcknowledged(), equalTo(true)); @@ -316,7 +316,7 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository without any path"); PutRepositoryResponse putRepositoryResponse = client.preparePutRepository("test-repo").setType("azure") .setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -337,8 +337,8 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository path [{}]", getRepositoryPath()); putRepositoryResponse = client.preparePutRepository("test-repo").setType("azure") .setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) - .put(Repository.BASE_PATH, getRepositoryPath()) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -363,7 +363,7 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository without any path"); PutRepositoryResponse putRepositoryResponse = client.preparePutRepository("test-repo").setType("azure") .setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -414,9 +414,9 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa try { PutRepositoryResponse putRepositoryResponse = client().admin().cluster().preparePutRepository("test-repo") .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, container) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.CHUNK_SIZE, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), container) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.CHUNK_SIZE_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); client().admin().cluster().prepareDeleteRepository("test-repo").get(); try { @@ -444,9 +444,9 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa logger.info("--> creating azure repository with path [{}]", getRepositoryPath()); PutRepositoryResponse putRepositoryResponse = client.admin().cluster().preparePutRepository("test-repo") .setType("azure").setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, getContainerName()) - .put(Repository.BASE_PATH, getRepositoryPath()) - .put(Repository.CHUNK_SIZE, randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) + .put(Repository.CONTAINER_SETTING.getKey(), getContainerName()) + .put(Repository.BASE_PATH_SETTING.getKey(), getRepositoryPath()) + .put(Repository.CHUNK_SIZE_SETTING.getKey(), randomIntBetween(1000, 10000), ByteSizeUnit.BYTES) ).get(); assertThat(putRepositoryResponse.isAcknowledged(), equalTo(true)); @@ -492,7 +492,7 @@ public class AzureSnapshotRestoreTests extends AbstractAzureWithThirdPartyTestCa try { client.preparePutRepository("test-repo").setType("azure") .setSettings(Settings.settingsBuilder() - .put(Repository.CONTAINER, container) + .put(Repository.CONTAINER_SETTING.getKey(), container) ).get(); fail("we should get a RepositoryVerificationException"); } catch (RepositoryVerificationException e) { From 24a841a07567f781a2d5f7cd6e2d5b7dcec6c0fc Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 29 Jan 2016 13:57:18 -0500 Subject: [PATCH 10/49] ShardId equality and hash code inconsistency This commit fixes an inconsistency between ShardId#equals(Object), ShardId#hashCode, and ShardId#compareTo(ShardId). In particular, ShardId#equals(Object) compared only the numerical shard ID and the index name, but did not compare the index UUID; a similar situation applies to ShardId#compareTo(ShardId). However, ShardId#hashCode incorporated the indexUUID into its calculation. This can lead to situations where two ShardIds compare as equal yet have different hash codes. Closes #16319 --- .../java/org/elasticsearch/index/shard/ShardId.java | 10 +++++++--- .../org/elasticsearch/index/shard/IndexShardTests.java | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/index/shard/ShardId.java b/core/src/main/java/org/elasticsearch/index/shard/ShardId.java index f021cb4c162..3dea5501c62 100644 --- a/core/src/main/java/org/elasticsearch/index/shard/ShardId.java +++ b/core/src/main/java/org/elasticsearch/index/shard/ShardId.java @@ -76,7 +76,7 @@ public class ShardId implements Streamable, Comparable { if (this == o) return true; if (o == null) return false; ShardId shardId1 = (ShardId) o; - return shardId == shardId1.shardId && index.getName().equals(shardId1.index.getName()); + return shardId == shardId1.shardId && index.equals(shardId1.index); } @Override @@ -112,8 +112,12 @@ public class ShardId implements Streamable, Comparable { @Override public int compareTo(ShardId o) { if (o.getId() == shardId) { - return index.getName().compareTo(o.getIndex().getName()); + int compare = index.getName().compareTo(o.getIndex().getName()); + if (compare != 0) { + return compare; + } + return index.getUUID().compareTo(o.getIndex().getUUID()); } return Integer.compare(shardId, o.getId()); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index ca0069e4eda..9a4e6a814a3 100644 --- a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -68,6 +68,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.env.ShardLock; +import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.NodeServicesProvider; @@ -250,7 +251,7 @@ public class IndexShardTests extends ESSingleNodeTestCase { ShardStateMetaData shardStateMetaData = load(logger, env.availableShardPaths(shard.shardId)); assertEquals(shardStateMetaData, getShardStateMetadata(shard)); - routing = TestShardRouting.newShardRouting(shard.shardId.getIndexName(), shard.shardId.id(), routing.currentNodeId(), null, routing.primary(), ShardRoutingState.INITIALIZING, shard.shardRouting.allocationId(), shard.shardRouting.version() + 1); + routing = TestShardRouting.newShardRouting(shard.shardId.getIndex(), shard.shardId.id(), routing.currentNodeId(), null, routing.primary(), ShardRoutingState.INITIALIZING, shard.shardRouting.allocationId(), shard.shardRouting.version() + 1); shard.updateRoutingEntry(routing, true); shard.deleteShardState(); From 7b5ed21d0dda532d43af51d3b5cd44ae6f4b82ed Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 1 Feb 2016 11:27:03 -0800 Subject: [PATCH 11/49] Remove reference to multicast docs, which no longer exist. --- docs/reference/migration/migrate_2_0/removals.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/reference/migration/migrate_2_0/removals.asciidoc b/docs/reference/migration/migrate_2_0/removals.asciidoc index 55f76c6f30e..82e1cd923f6 100644 --- a/docs/reference/migration/migrate_2_0/removals.asciidoc +++ b/docs/reference/migration/migrate_2_0/removals.asciidoc @@ -58,8 +58,6 @@ still need to use multicast discovery, you can install the plugin with: ./bin/plugin install discovery-multicast ------------------ -See {plugins}/discovery-multicast.html for more information. - ==== `_shutdown` API The `_shutdown` API has been removed without a replacement. Nodes should be From e3fd6c2a00b1cdc6df5d23cf956d4110ade966b5 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Mon, 1 Feb 2016 16:16:04 -0800 Subject: [PATCH 12/49] Removed ..= token from the Lexer. Fixed related tests. --- .../src/main/antlr/PainlessLexer.g4 | 1 - .../elasticsearch/painless/PainlessLexer.java | 478 +++++++++--------- .../painless/PainlessParser.java | 155 +++--- .../painless/NoSemiColonTest.java | 36 +- 4 files changed, 325 insertions(+), 345 deletions(-) diff --git a/plugins/lang-painless/src/main/antlr/PainlessLexer.g4 b/plugins/lang-painless/src/main/antlr/PainlessLexer.g4 index 11cd97cc9e3..866bbd752c8 100644 --- a/plugins/lang-painless/src/main/antlr/PainlessLexer.g4 +++ b/plugins/lang-painless/src/main/antlr/PainlessLexer.g4 @@ -96,7 +96,6 @@ AOR: '|='; ALSH: '<<='; ARSH: '>>='; AUSH: '>>>='; -ACAT: '..='; OCTAL: '0' [0-7]+ [lL]?; HEX: '0' [xX] [0-9a-fA-F]+ [lL]?; diff --git a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java index 3a01626d872..941615958a7 100644 --- a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java +++ b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java @@ -1,19 +1,16 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless; -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.Lexer; -import org.antlr.v4.runtime.RuleContext; -import org.antlr.v4.runtime.RuntimeMetaData; -import org.antlr.v4.runtime.Vocabulary; -import org.antlr.v4.runtime.VocabularyImpl; -import org.antlr.v4.runtime.atn.ATN; -import org.antlr.v4.runtime.atn.ATNDeserializer; -import org.antlr.v4.runtime.atn.LexerATNSimulator; -import org.antlr.v4.runtime.atn.PredictionContextCache; -import org.antlr.v4.runtime.dfa.DFA; + import java.util.Set; -import java.util.Set; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.atn.*; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.misc.*; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) class PainlessLexer extends Lexer { @@ -23,53 +20,52 @@ class PainlessLexer extends Lexer { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, - BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, - MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, - LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, - BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, - AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, - ALSH=60, ARSH=61, AUSH=62, ACAT=63, OCTAL=64, HEX=65, INTEGER=66, DECIMAL=67, - STRING=68, CHAR=69, TRUE=70, FALSE=71, NULL=72, TYPE=73, ID=74, EXTINTEGER=75, - EXTID=76; + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, + BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, + MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, + LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, + BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, + AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, + ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, + CHAR=68, TRUE=69, FALSE=70, NULL=71, TYPE=72, ID=73, EXTINTEGER=74, EXTID=75; public static final int EXT = 1; public static String[] modeNames = { "DEFAULT_MODE", "EXT" }; public static final String[] ruleNames = { - "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", - "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "ACAT", - "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", - "NULL", "TYPE", "GENERIC", "ID", "EXTINTEGER", "EXTID" + "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", + "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + "TYPE", "GENERIC", "ID", "EXTINTEGER", "EXTID" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", - "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", - "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", - "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", - "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", - "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", - "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", "'..='", null, - null, null, null, null, null, "'true'", "'false'", "'null'" + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", + "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", + "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", + "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", + "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", + "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", + "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, + null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "ACAT", - "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", - "NULL", "TYPE", "ID", "EXTINTEGER", "EXTID" + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + "TYPE", "ID", "EXTINTEGER", "EXTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @@ -135,13 +131,13 @@ class PainlessLexer extends Lexer { @Override public void action(RuleContext _localctx, int ruleIndex, int actionIndex) { switch (ruleIndex) { - case 67: + case 66: STRING_action((RuleContext)_localctx, actionIndex); break; - case 68: + case 67: CHAR_action((RuleContext)_localctx, actionIndex); break; - case 72: + case 71: TYPE_action((RuleContext)_localctx, actionIndex); break; } @@ -170,7 +166,7 @@ class PainlessLexer extends Lexer { @Override public boolean sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { - case 72: + case 71: return TYPE_sempred((RuleContext)_localctx, predIndex); } return true; @@ -184,7 +180,7 @@ class PainlessLexer extends Lexer { } public static final String _serializedATN = - "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\2N\u0236\b\1\b\1\4"+ + "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\2M\u0230\b\1\b\1\4"+ "\2\t\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n"+ "\4\13\t\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22"+ "\t\22\4\23\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30\4\31"+ @@ -193,195 +189,193 @@ class PainlessLexer extends Lexer { "+\4,\t,\4-\t-\4.\t.\4/\t/\4\60\t\60\4\61\t\61\4\62\t\62\4\63\t\63\4\64"+ "\t\64\4\65\t\65\4\66\t\66\4\67\t\67\48\t8\49\t9\4:\t:\4;\t;\4<\t<\4=\t"+ "=\4>\t>\4?\t?\4@\t@\4A\tA\4B\tB\4C\tC\4D\tD\4E\tE\4F\tF\4G\tG\4H\tH\4"+ - "I\tI\4J\tJ\4K\tK\4L\tL\4M\tM\4N\tN\3\2\6\2\u00a0\n\2\r\2\16\2\u00a1\3"+ - "\2\3\2\3\3\3\3\3\3\3\3\7\3\u00aa\n\3\f\3\16\3\u00ad\13\3\3\3\3\3\3\3\3"+ - "\3\3\3\7\3\u00b4\n\3\f\3\16\3\u00b7\13\3\3\3\3\3\5\3\u00bb\n\3\3\3\3\3"+ - "\3\4\3\4\3\5\3\5\3\6\3\6\3\7\3\7\3\b\3\b\3\t\3\t\3\n\3\n\3\n\3\n\3\13"+ - "\3\13\3\f\3\f\3\r\3\r\3\r\3\16\3\16\3\16\3\16\3\16\3\17\3\17\3\17\3\17"+ - "\3\17\3\17\3\20\3\20\3\20\3\21\3\21\3\21\3\21\3\22\3\22\3\22\3\22\3\22"+ - "\3\22\3\22\3\22\3\22\3\23\3\23\3\23\3\23\3\23\3\23\3\24\3\24\3\24\3\24"+ - "\3\24\3\24\3\24\3\25\3\25\3\25\3\25\3\26\3\26\3\26\3\26\3\27\3\27\3\27"+ - "\3\27\3\27\3\27\3\30\3\30\3\30\3\30\3\30\3\30\3\31\3\31\3\32\3\32\3\33"+ - "\3\33\3\34\3\34\3\35\3\35\3\36\3\36\3\37\3\37\3 \3 \3 \3!\3!\3!\3\"\3"+ - "\"\3\"\3\"\3#\3#\3$\3$\3$\3%\3%\3&\3&\3&\3\'\3\'\3\'\3(\3(\3(\3(\3)\3"+ - ")\3)\3*\3*\3*\3*\3+\3+\3,\3,\3-\3-\3.\3.\3.\3/\3/\3/\3\60\3\60\3\61\3"+ - "\61\3\62\3\62\3\62\3\63\3\63\3\63\3\64\3\64\3\65\3\65\3\65\3\66\3\66\3"+ - "\66\3\67\3\67\3\67\38\38\38\39\39\39\3:\3:\3:\3;\3;\3;\3<\3<\3<\3=\3="+ - "\3=\3=\3>\3>\3>\3>\3?\3?\3?\3?\3?\3@\3@\3@\3@\3A\3A\6A\u0185\nA\rA\16"+ - "A\u0186\3A\5A\u018a\nA\3B\3B\3B\6B\u018f\nB\rB\16B\u0190\3B\5B\u0194\n"+ - "B\3C\3C\3C\7C\u0199\nC\fC\16C\u019c\13C\5C\u019e\nC\3C\5C\u01a1\nC\3D"+ - "\3D\3D\7D\u01a6\nD\fD\16D\u01a9\13D\5D\u01ab\nD\3D\3D\7D\u01af\nD\fD\16"+ - "D\u01b2\13D\3D\3D\5D\u01b6\nD\3D\6D\u01b9\nD\rD\16D\u01ba\5D\u01bd\nD"+ - "\3D\5D\u01c0\nD\3E\3E\3E\3E\3E\3E\7E\u01c8\nE\fE\16E\u01cb\13E\3E\3E\3"+ - "E\3F\3F\3F\3F\3F\3G\3G\3G\3G\3G\3H\3H\3H\3H\3H\3H\3I\3I\3I\3I\3I\3J\3"+ - "J\5J\u01e7\nJ\3J\3J\3J\3K\7K\u01ed\nK\fK\16K\u01f0\13K\3K\3K\7K\u01f4"+ - "\nK\fK\16K\u01f7\13K\3K\3K\5K\u01fb\nK\3K\7K\u01fe\nK\fK\16K\u0201\13"+ - "K\3K\3K\7K\u0205\nK\fK\16K\u0208\13K\3K\3K\5K\u020c\nK\3K\7K\u020f\nK"+ - "\fK\16K\u0212\13K\7K\u0214\nK\fK\16K\u0217\13K\3K\3K\3L\3L\7L\u021d\n"+ - "L\fL\16L\u0220\13L\3M\3M\3M\7M\u0225\nM\fM\16M\u0228\13M\5M\u022a\nM\3"+ - "M\3M\3N\3N\7N\u0230\nN\fN\16N\u0233\13N\3N\3N\5\u00ab\u00b5\u01c9\2O\4"+ - "\3\6\4\b\5\n\6\f\7\16\b\20\t\22\n\24\13\26\f\30\r\32\16\34\17\36\20 \21"+ - "\"\22$\23&\24(\25*\26,\27.\30\60\31\62\32\64\33\66\348\35:\36<\37> @!"+ - "B\"D#F$H%J&L\'N(P)R*T+V,X-Z.\\/^\60`\61b\62d\63f\64h\65j\66l\67n8p9r:"+ - "t;v|?~@\u0080A\u0082B\u0084C\u0086D\u0088E\u008aF\u008cG\u008eH\u0090"+ - "I\u0092J\u0094K\u0096\2\u0098L\u009aM\u009cN\4\2\3\21\5\2\13\f\17\17\""+ - "\"\4\2\f\f\17\17\3\2\629\4\2NNnn\4\2ZZzz\5\2\62;CHch\3\2\63;\3\2\62;\b"+ - "\2FFHHNNffhhnn\4\2GGgg\4\2--//\4\2HHhh\4\2$$^^\5\2C\\aac|\6\2\62;C\\a"+ - "ac|\u0255\2\4\3\2\2\2\2\6\3\2\2\2\2\b\3\2\2\2\2\n\3\2\2\2\2\f\3\2\2\2"+ - "\2\16\3\2\2\2\2\20\3\2\2\2\2\22\3\2\2\2\2\24\3\2\2\2\2\26\3\2\2\2\2\30"+ - "\3\2\2\2\2\32\3\2\2\2\2\34\3\2\2\2\2\36\3\2\2\2\2 \3\2\2\2\2\"\3\2\2\2"+ - "\2$\3\2\2\2\2&\3\2\2\2\2(\3\2\2\2\2*\3\2\2\2\2,\3\2\2\2\2.\3\2\2\2\2\60"+ - "\3\2\2\2\2\62\3\2\2\2\2\64\3\2\2\2\2\66\3\2\2\2\28\3\2\2\2\2:\3\2\2\2"+ - "\2<\3\2\2\2\2>\3\2\2\2\2@\3\2\2\2\2B\3\2\2\2\2D\3\2\2\2\2F\3\2\2\2\2H"+ - "\3\2\2\2\2J\3\2\2\2\2L\3\2\2\2\2N\3\2\2\2\2P\3\2\2\2\2R\3\2\2\2\2T\3\2"+ - "\2\2\2V\3\2\2\2\2X\3\2\2\2\2Z\3\2\2\2\2\\\3\2\2\2\2^\3\2\2\2\2`\3\2\2"+ - "\2\2b\3\2\2\2\2d\3\2\2\2\2f\3\2\2\2\2h\3\2\2\2\2j\3\2\2\2\2l\3\2\2\2\2"+ - "n\3\2\2\2\2p\3\2\2\2\2r\3\2\2\2\2t\3\2\2\2\2v\3\2\2\2\2x\3\2\2\2\2z\3"+ - "\2\2\2\2|\3\2\2\2\2~\3\2\2\2\2\u0080\3\2\2\2\2\u0082\3\2\2\2\2\u0084\3"+ - "\2\2\2\2\u0086\3\2\2\2\2\u0088\3\2\2\2\2\u008a\3\2\2\2\2\u008c\3\2\2\2"+ - "\2\u008e\3\2\2\2\2\u0090\3\2\2\2\2\u0092\3\2\2\2\2\u0094\3\2\2\2\2\u0098"+ - "\3\2\2\2\3\u009a\3\2\2\2\3\u009c\3\2\2\2\4\u009f\3\2\2\2\6\u00ba\3\2\2"+ - "\2\b\u00be\3\2\2\2\n\u00c0\3\2\2\2\f\u00c2\3\2\2\2\16\u00c4\3\2\2\2\20"+ - "\u00c6\3\2\2\2\22\u00c8\3\2\2\2\24\u00ca\3\2\2\2\26\u00ce\3\2\2\2\30\u00d0"+ - "\3\2\2\2\32\u00d2\3\2\2\2\34\u00d5\3\2\2\2\36\u00da\3\2\2\2 \u00e0\3\2"+ - "\2\2\"\u00e3\3\2\2\2$\u00e7\3\2\2\2&\u00f0\3\2\2\2(\u00f6\3\2\2\2*\u00fd"+ - "\3\2\2\2,\u0101\3\2\2\2.\u0105\3\2\2\2\60\u010b\3\2\2\2\62\u0111\3\2\2"+ - "\2\64\u0113\3\2\2\2\66\u0115\3\2\2\28\u0117\3\2\2\2:\u0119\3\2\2\2<\u011b"+ - "\3\2\2\2>\u011d\3\2\2\2@\u011f\3\2\2\2B\u0122\3\2\2\2D\u0125\3\2\2\2F"+ - "\u0129\3\2\2\2H\u012b\3\2\2\2J\u012e\3\2\2\2L\u0130\3\2\2\2N\u0133\3\2"+ - "\2\2P\u0136\3\2\2\2R\u013a\3\2\2\2T\u013d\3\2\2\2V\u0141\3\2\2\2X\u0143"+ - "\3\2\2\2Z\u0145\3\2\2\2\\\u0147\3\2\2\2^\u014a\3\2\2\2`\u014d\3\2\2\2"+ - "b\u014f\3\2\2\2d\u0151\3\2\2\2f\u0154\3\2\2\2h\u0157\3\2\2\2j\u0159\3"+ - "\2\2\2l\u015c\3\2\2\2n\u015f\3\2\2\2p\u0162\3\2\2\2r\u0165\3\2\2\2t\u0168"+ - "\3\2\2\2v\u016b\3\2\2\2x\u016e\3\2\2\2z\u0171\3\2\2\2|\u0175\3\2\2\2~"+ - "\u0179\3\2\2\2\u0080\u017e\3\2\2\2\u0082\u0182\3\2\2\2\u0084\u018b\3\2"+ - "\2\2\u0086\u019d\3\2\2\2\u0088\u01aa\3\2\2\2\u008a\u01c1\3\2\2\2\u008c"+ - "\u01cf\3\2\2\2\u008e\u01d4\3\2\2\2\u0090\u01d9\3\2\2\2\u0092\u01df\3\2"+ - "\2\2\u0094\u01e4\3\2\2\2\u0096\u01ee\3\2\2\2\u0098\u021a\3\2\2\2\u009a"+ - "\u0229\3\2\2\2\u009c\u022d\3\2\2\2\u009e\u00a0\t\2\2\2\u009f\u009e\3\2"+ - "\2\2\u00a0\u00a1\3\2\2\2\u00a1\u009f\3\2\2\2\u00a1\u00a2\3\2\2\2\u00a2"+ - "\u00a3\3\2\2\2\u00a3\u00a4\b\2\2\2\u00a4\5\3\2\2\2\u00a5\u00a6\7\61\2"+ - "\2\u00a6\u00a7\7\61\2\2\u00a7\u00ab\3\2\2\2\u00a8\u00aa\13\2\2\2\u00a9"+ - "\u00a8\3\2\2\2\u00aa\u00ad\3\2\2\2\u00ab\u00ac\3\2\2\2\u00ab\u00a9\3\2"+ - "\2\2\u00ac\u00ae\3\2\2\2\u00ad\u00ab\3\2\2\2\u00ae\u00bb\t\3\2\2\u00af"+ - "\u00b0\7\61\2\2\u00b0\u00b1\7,\2\2\u00b1\u00b5\3\2\2\2\u00b2\u00b4\13"+ - "\2\2\2\u00b3\u00b2\3\2\2\2\u00b4\u00b7\3\2\2\2\u00b5\u00b6\3\2\2\2\u00b5"+ - "\u00b3\3\2\2\2\u00b6\u00b8\3\2\2\2\u00b7\u00b5\3\2\2\2\u00b8\u00b9\7,"+ - "\2\2\u00b9\u00bb\7\61\2\2\u00ba\u00a5\3\2\2\2\u00ba\u00af\3\2\2\2\u00bb"+ - "\u00bc\3\2\2\2\u00bc\u00bd\b\3\2\2\u00bd\7\3\2\2\2\u00be\u00bf\7}\2\2"+ - "\u00bf\t\3\2\2\2\u00c0\u00c1\7\177\2\2\u00c1\13\3\2\2\2\u00c2\u00c3\7"+ - "]\2\2\u00c3\r\3\2\2\2\u00c4\u00c5\7_\2\2\u00c5\17\3\2\2\2\u00c6\u00c7"+ - "\7*\2\2\u00c7\21\3\2\2\2\u00c8\u00c9\7+\2\2\u00c9\23\3\2\2\2\u00ca\u00cb"+ - "\7\60\2\2\u00cb\u00cc\3\2\2\2\u00cc\u00cd\b\n\3\2\u00cd\25\3\2\2\2\u00ce"+ - "\u00cf\7.\2\2\u00cf\27\3\2\2\2\u00d0\u00d1\7=\2\2\u00d1\31\3\2\2\2\u00d2"+ - "\u00d3\7k\2\2\u00d3\u00d4\7h\2\2\u00d4\33\3\2\2\2\u00d5\u00d6\7g\2\2\u00d6"+ - "\u00d7\7n\2\2\u00d7\u00d8\7u\2\2\u00d8\u00d9\7g\2\2\u00d9\35\3\2\2\2\u00da"+ - "\u00db\7y\2\2\u00db\u00dc\7j\2\2\u00dc\u00dd\7k\2\2\u00dd\u00de\7n\2\2"+ - "\u00de\u00df\7g\2\2\u00df\37\3\2\2\2\u00e0\u00e1\7f\2\2\u00e1\u00e2\7"+ - "q\2\2\u00e2!\3\2\2\2\u00e3\u00e4\7h\2\2\u00e4\u00e5\7q\2\2\u00e5\u00e6"+ - "\7t\2\2\u00e6#\3\2\2\2\u00e7\u00e8\7e\2\2\u00e8\u00e9\7q\2\2\u00e9\u00ea"+ - "\7p\2\2\u00ea\u00eb\7v\2\2\u00eb\u00ec\7k\2\2\u00ec\u00ed\7p\2\2\u00ed"+ - "\u00ee\7w\2\2\u00ee\u00ef\7g\2\2\u00ef%\3\2\2\2\u00f0\u00f1\7d\2\2\u00f1"+ - "\u00f2\7t\2\2\u00f2\u00f3\7g\2\2\u00f3\u00f4\7c\2\2\u00f4\u00f5\7m\2\2"+ - "\u00f5\'\3\2\2\2\u00f6\u00f7\7t\2\2\u00f7\u00f8\7g\2\2\u00f8\u00f9\7v"+ - "\2\2\u00f9\u00fa\7w\2\2\u00fa\u00fb\7t\2\2\u00fb\u00fc\7p\2\2\u00fc)\3"+ - "\2\2\2\u00fd\u00fe\7p\2\2\u00fe\u00ff\7g\2\2\u00ff\u0100\7y\2\2\u0100"+ - "+\3\2\2\2\u0101\u0102\7v\2\2\u0102\u0103\7t\2\2\u0103\u0104\7{\2\2\u0104"+ - "-\3\2\2\2\u0105\u0106\7e\2\2\u0106\u0107\7c\2\2\u0107\u0108\7v\2\2\u0108"+ - "\u0109\7e\2\2\u0109\u010a\7j\2\2\u010a/\3\2\2\2\u010b\u010c\7v\2\2\u010c"+ - "\u010d\7j\2\2\u010d\u010e\7t\2\2\u010e\u010f\7q\2\2\u010f\u0110\7y\2\2"+ - "\u0110\61\3\2\2\2\u0111\u0112\7#\2\2\u0112\63\3\2\2\2\u0113\u0114\7\u0080"+ - "\2\2\u0114\65\3\2\2\2\u0115\u0116\7,\2\2\u0116\67\3\2\2\2\u0117\u0118"+ - "\7\61\2\2\u01189\3\2\2\2\u0119\u011a\7\'\2\2\u011a;\3\2\2\2\u011b\u011c"+ - "\7-\2\2\u011c=\3\2\2\2\u011d\u011e\7/\2\2\u011e?\3\2\2\2\u011f\u0120\7"+ - ">\2\2\u0120\u0121\7>\2\2\u0121A\3\2\2\2\u0122\u0123\7@\2\2\u0123\u0124"+ - "\7@\2\2\u0124C\3\2\2\2\u0125\u0126\7@\2\2\u0126\u0127\7@\2\2\u0127\u0128"+ - "\7@\2\2\u0128E\3\2\2\2\u0129\u012a\7>\2\2\u012aG\3\2\2\2\u012b\u012c\7"+ - ">\2\2\u012c\u012d\7?\2\2\u012dI\3\2\2\2\u012e\u012f\7@\2\2\u012fK\3\2"+ - "\2\2\u0130\u0131\7@\2\2\u0131\u0132\7?\2\2\u0132M\3\2\2\2\u0133\u0134"+ - "\7?\2\2\u0134\u0135\7?\2\2\u0135O\3\2\2\2\u0136\u0137\7?\2\2\u0137\u0138"+ - "\7?\2\2\u0138\u0139\7?\2\2\u0139Q\3\2\2\2\u013a\u013b\7#\2\2\u013b\u013c"+ - "\7?\2\2\u013cS\3\2\2\2\u013d\u013e\7#\2\2\u013e\u013f\7?\2\2\u013f\u0140"+ - "\7?\2\2\u0140U\3\2\2\2\u0141\u0142\7(\2\2\u0142W\3\2\2\2\u0143\u0144\7"+ - "`\2\2\u0144Y\3\2\2\2\u0145\u0146\7~\2\2\u0146[\3\2\2\2\u0147\u0148\7("+ - "\2\2\u0148\u0149\7(\2\2\u0149]\3\2\2\2\u014a\u014b\7~\2\2\u014b\u014c"+ - "\7~\2\2\u014c_\3\2\2\2\u014d\u014e\7A\2\2\u014ea\3\2\2\2\u014f\u0150\7"+ - "<\2\2\u0150c\3\2\2\2\u0151\u0152\7-\2\2\u0152\u0153\7-\2\2\u0153e\3\2"+ - "\2\2\u0154\u0155\7/\2\2\u0155\u0156\7/\2\2\u0156g\3\2\2\2\u0157\u0158"+ - "\7?\2\2\u0158i\3\2\2\2\u0159\u015a\7-\2\2\u015a\u015b\7?\2\2\u015bk\3"+ - "\2\2\2\u015c\u015d\7/\2\2\u015d\u015e\7?\2\2\u015em\3\2\2\2\u015f\u0160"+ - "\7,\2\2\u0160\u0161\7?\2\2\u0161o\3\2\2\2\u0162\u0163\7\61\2\2\u0163\u0164"+ - "\7?\2\2\u0164q\3\2\2\2\u0165\u0166\7\'\2\2\u0166\u0167\7?\2\2\u0167s\3"+ - "\2\2\2\u0168\u0169\7(\2\2\u0169\u016a\7?\2\2\u016au\3\2\2\2\u016b\u016c"+ - "\7`\2\2\u016c\u016d\7?\2\2\u016dw\3\2\2\2\u016e\u016f\7~\2\2\u016f\u0170"+ - "\7?\2\2\u0170y\3\2\2\2\u0171\u0172\7>\2\2\u0172\u0173\7>\2\2\u0173\u0174"+ - "\7?\2\2\u0174{\3\2\2\2\u0175\u0176\7@\2\2\u0176\u0177\7@\2\2\u0177\u0178"+ - "\7?\2\2\u0178}\3\2\2\2\u0179\u017a\7@\2\2\u017a\u017b\7@\2\2\u017b\u017c"+ - "\7@\2\2\u017c\u017d\7?\2\2\u017d\177\3\2\2\2\u017e\u017f\7\60\2\2\u017f"+ - "\u0180\7\60\2\2\u0180\u0181\7?\2\2\u0181\u0081\3\2\2\2\u0182\u0184\7\62"+ - "\2\2\u0183\u0185\t\4\2\2\u0184\u0183\3\2\2\2\u0185\u0186\3\2\2\2\u0186"+ - "\u0184\3\2\2\2\u0186\u0187\3\2\2\2\u0187\u0189\3\2\2\2\u0188\u018a\t\5"+ - "\2\2\u0189\u0188\3\2\2\2\u0189\u018a\3\2\2\2\u018a\u0083\3\2\2\2\u018b"+ - "\u018c\7\62\2\2\u018c\u018e\t\6\2\2\u018d\u018f\t\7\2\2\u018e\u018d\3"+ - "\2\2\2\u018f\u0190\3\2\2\2\u0190\u018e\3\2\2\2\u0190\u0191\3\2\2\2\u0191"+ - "\u0193\3\2\2\2\u0192\u0194\t\5\2\2\u0193\u0192\3\2\2\2\u0193\u0194\3\2"+ - "\2\2\u0194\u0085\3\2\2\2\u0195\u019e\7\62\2\2\u0196\u019a\t\b\2\2\u0197"+ - "\u0199\t\t\2\2\u0198\u0197\3\2\2\2\u0199\u019c\3\2\2\2\u019a\u0198\3\2"+ - "\2\2\u019a\u019b\3\2\2\2\u019b\u019e\3\2\2\2\u019c\u019a\3\2\2\2\u019d"+ - "\u0195\3\2\2\2\u019d\u0196\3\2\2\2\u019e\u01a0\3\2\2\2\u019f\u01a1\t\n"+ - "\2\2\u01a0\u019f\3\2\2\2\u01a0\u01a1\3\2\2\2\u01a1\u0087\3\2\2\2\u01a2"+ - "\u01ab\7\62\2\2\u01a3\u01a7\t\b\2\2\u01a4\u01a6\t\t\2\2\u01a5\u01a4\3"+ - "\2\2\2\u01a6\u01a9\3\2\2\2\u01a7\u01a5\3\2\2\2\u01a7\u01a8\3\2\2\2\u01a8"+ - "\u01ab\3\2\2\2\u01a9\u01a7\3\2\2\2\u01aa\u01a2\3\2\2\2\u01aa\u01a3\3\2"+ - "\2\2\u01ab\u01ac\3\2\2\2\u01ac\u01b0\5\24\n\2\u01ad\u01af\t\t\2\2\u01ae"+ - "\u01ad\3\2\2\2\u01af\u01b2\3\2\2\2\u01b0\u01ae\3\2\2\2\u01b0\u01b1\3\2"+ - "\2\2\u01b1\u01bc\3\2\2\2\u01b2\u01b0\3\2\2\2\u01b3\u01b5\t\13\2\2\u01b4"+ - "\u01b6\t\f\2\2\u01b5\u01b4\3\2\2\2\u01b5\u01b6\3\2\2\2\u01b6\u01b8\3\2"+ - "\2\2\u01b7\u01b9\t\t\2\2\u01b8\u01b7\3\2\2\2\u01b9\u01ba\3\2\2\2\u01ba"+ - "\u01b8\3\2\2\2\u01ba\u01bb\3\2\2\2\u01bb\u01bd\3\2\2\2\u01bc\u01b3\3\2"+ - "\2\2\u01bc\u01bd\3\2\2\2\u01bd\u01bf\3\2\2\2\u01be\u01c0\t\r\2\2\u01bf"+ - "\u01be\3\2\2\2\u01bf\u01c0\3\2\2\2\u01c0\u0089\3\2\2\2\u01c1\u01c9\7$"+ - "\2\2\u01c2\u01c3\7^\2\2\u01c3\u01c8\7$\2\2\u01c4\u01c5\7^\2\2\u01c5\u01c8"+ - "\7^\2\2\u01c6\u01c8\n\16\2\2\u01c7\u01c2\3\2\2\2\u01c7\u01c4\3\2\2\2\u01c7"+ - "\u01c6\3\2\2\2\u01c8\u01cb\3\2\2\2\u01c9\u01ca\3\2\2\2\u01c9\u01c7\3\2"+ - "\2\2\u01ca\u01cc\3\2\2\2\u01cb\u01c9\3\2\2\2\u01cc\u01cd\7$\2\2\u01cd"+ - "\u01ce\bE\4\2\u01ce\u008b\3\2\2\2\u01cf\u01d0\7)\2\2\u01d0\u01d1\13\2"+ - "\2\2\u01d1\u01d2\7)\2\2\u01d2\u01d3\bF\5\2\u01d3\u008d\3\2\2\2\u01d4\u01d5"+ - "\7v\2\2\u01d5\u01d6\7t\2\2\u01d6\u01d7\7w\2\2\u01d7\u01d8\7g\2\2\u01d8"+ - "\u008f\3\2\2\2\u01d9\u01da\7h\2\2\u01da\u01db\7c\2\2\u01db\u01dc\7n\2"+ - "\2\u01dc\u01dd\7u\2\2\u01dd\u01de\7g\2\2\u01de\u0091\3\2\2\2\u01df\u01e0"+ - "\7p\2\2\u01e0\u01e1\7w\2\2\u01e1\u01e2\7n\2\2\u01e2\u01e3\7n\2\2\u01e3"+ - "\u0093\3\2\2\2\u01e4\u01e6\5\u0098L\2\u01e5\u01e7\5\u0096K\2\u01e6\u01e5"+ - "\3\2\2\2\u01e6\u01e7\3\2\2\2\u01e7\u01e8\3\2\2\2\u01e8\u01e9\6J\2\2\u01e9"+ - "\u01ea\bJ\6\2\u01ea\u0095\3\2\2\2\u01eb\u01ed\7\"\2\2\u01ec\u01eb\3\2"+ - "\2\2\u01ed\u01f0\3\2\2\2\u01ee\u01ec\3\2\2\2\u01ee\u01ef\3\2\2\2\u01ef"+ - "\u01f1\3\2\2\2\u01f0\u01ee\3\2\2\2\u01f1\u01f5\7>\2\2\u01f2\u01f4\7\""+ - "\2\2\u01f3\u01f2\3\2\2\2\u01f4\u01f7\3\2\2\2\u01f5\u01f3\3\2\2\2\u01f5"+ - "\u01f6\3\2\2\2\u01f6\u01f8\3\2\2\2\u01f7\u01f5\3\2\2\2\u01f8\u01fa\5\u0098"+ - "L\2\u01f9\u01fb\5\u0096K\2\u01fa\u01f9\3\2\2\2\u01fa\u01fb\3\2\2\2\u01fb"+ - "\u01ff\3\2\2\2\u01fc\u01fe\7\"\2\2\u01fd\u01fc\3\2\2\2\u01fe\u0201\3\2"+ - "\2\2\u01ff\u01fd\3\2\2\2\u01ff\u0200\3\2\2\2\u0200\u0215\3\2\2\2\u0201"+ - "\u01ff\3\2\2\2\u0202\u0206\5\26\13\2\u0203\u0205\7\"\2\2\u0204\u0203\3"+ - "\2\2\2\u0205\u0208\3\2\2\2\u0206\u0204\3\2\2\2\u0206\u0207\3\2\2\2\u0207"+ - "\u0209\3\2\2\2\u0208\u0206\3\2\2\2\u0209\u020b\5\u0098L\2\u020a\u020c"+ - "\5\u0096K\2\u020b\u020a\3\2\2\2\u020b\u020c\3\2\2\2\u020c\u0210\3\2\2"+ - "\2\u020d\u020f\7\"\2\2\u020e\u020d\3\2\2\2\u020f\u0212\3\2\2\2\u0210\u020e"+ - "\3\2\2\2\u0210\u0211\3\2\2\2\u0211\u0214\3\2\2\2\u0212\u0210\3\2\2\2\u0213"+ - "\u0202\3\2\2\2\u0214\u0217\3\2\2\2\u0215\u0213\3\2\2\2\u0215\u0216\3\2"+ - "\2\2\u0216\u0218\3\2\2\2\u0217\u0215\3\2\2\2\u0218\u0219\7@\2\2\u0219"+ - "\u0097\3\2\2\2\u021a\u021e\t\17\2\2\u021b\u021d\t\20\2\2\u021c\u021b\3"+ - "\2\2\2\u021d\u0220\3\2\2\2\u021e\u021c\3\2\2\2\u021e\u021f\3\2\2\2\u021f"+ - "\u0099\3\2\2\2\u0220\u021e\3\2\2\2\u0221\u022a\7\62\2\2\u0222\u0226\t"+ - "\b\2\2\u0223\u0225\t\t\2\2\u0224\u0223\3\2\2\2\u0225\u0228\3\2\2\2\u0226"+ - "\u0224\3\2\2\2\u0226\u0227\3\2\2\2\u0227\u022a\3\2\2\2\u0228\u0226\3\2"+ - "\2\2\u0229\u0221\3\2\2\2\u0229\u0222\3\2\2\2\u022a\u022b\3\2\2\2\u022b"+ - "\u022c\bM\7\2\u022c\u009b\3\2\2\2\u022d\u0231\t\17\2\2\u022e\u0230\t\20"+ - "\2\2\u022f\u022e\3\2\2\2\u0230\u0233\3\2\2\2\u0231\u022f\3\2\2\2\u0231"+ - "\u0232\3\2\2\2\u0232\u0234\3\2\2\2\u0233\u0231\3\2\2\2\u0234\u0235\bN"+ - "\7\2\u0235\u009d\3\2\2\2%\2\3\u00a1\u00ab\u00b5\u00ba\u0186\u0189\u0190"+ - "\u0193\u019a\u019d\u01a0\u01a7\u01aa\u01b0\u01b5\u01ba\u01bc\u01bf\u01c7"+ - "\u01c9\u01e6\u01ee\u01f5\u01fa\u01ff\u0206\u020b\u0210\u0215\u021e\u0226"+ - "\u0229\u0231\b\b\2\2\4\3\2\3E\2\3F\3\3J\4\4\2\2"; + "I\tI\4J\tJ\4K\tK\4L\tL\4M\tM\3\2\6\2\u009e\n\2\r\2\16\2\u009f\3\2\3\2"+ + "\3\3\3\3\3\3\3\3\7\3\u00a8\n\3\f\3\16\3\u00ab\13\3\3\3\3\3\3\3\3\3\3\3"+ + "\7\3\u00b2\n\3\f\3\16\3\u00b5\13\3\3\3\3\3\5\3\u00b9\n\3\3\3\3\3\3\4\3"+ + "\4\3\5\3\5\3\6\3\6\3\7\3\7\3\b\3\b\3\t\3\t\3\n\3\n\3\n\3\n\3\13\3\13\3"+ + "\f\3\f\3\r\3\r\3\r\3\16\3\16\3\16\3\16\3\16\3\17\3\17\3\17\3\17\3\17\3"+ + "\17\3\20\3\20\3\20\3\21\3\21\3\21\3\21\3\22\3\22\3\22\3\22\3\22\3\22\3"+ + "\22\3\22\3\22\3\23\3\23\3\23\3\23\3\23\3\23\3\24\3\24\3\24\3\24\3\24\3"+ + "\24\3\24\3\25\3\25\3\25\3\25\3\26\3\26\3\26\3\26\3\27\3\27\3\27\3\27\3"+ + "\27\3\27\3\30\3\30\3\30\3\30\3\30\3\30\3\31\3\31\3\32\3\32\3\33\3\33\3"+ + "\34\3\34\3\35\3\35\3\36\3\36\3\37\3\37\3 \3 \3 \3!\3!\3!\3\"\3\"\3\"\3"+ + "\"\3#\3#\3$\3$\3$\3%\3%\3&\3&\3&\3\'\3\'\3\'\3(\3(\3(\3(\3)\3)\3)\3*\3"+ + "*\3*\3*\3+\3+\3,\3,\3-\3-\3.\3.\3.\3/\3/\3/\3\60\3\60\3\61\3\61\3\62\3"+ + "\62\3\62\3\63\3\63\3\63\3\64\3\64\3\65\3\65\3\65\3\66\3\66\3\66\3\67\3"+ + "\67\3\67\38\38\38\39\39\39\3:\3:\3:\3;\3;\3;\3<\3<\3<\3=\3=\3=\3=\3>\3"+ + ">\3>\3>\3?\3?\3?\3?\3?\3@\3@\6@\u017f\n@\r@\16@\u0180\3@\5@\u0184\n@\3"+ + "A\3A\3A\6A\u0189\nA\rA\16A\u018a\3A\5A\u018e\nA\3B\3B\3B\7B\u0193\nB\f"+ + "B\16B\u0196\13B\5B\u0198\nB\3B\5B\u019b\nB\3C\3C\3C\7C\u01a0\nC\fC\16"+ + "C\u01a3\13C\5C\u01a5\nC\3C\3C\7C\u01a9\nC\fC\16C\u01ac\13C\3C\3C\5C\u01b0"+ + "\nC\3C\6C\u01b3\nC\rC\16C\u01b4\5C\u01b7\nC\3C\5C\u01ba\nC\3D\3D\3D\3"+ + "D\3D\3D\7D\u01c2\nD\fD\16D\u01c5\13D\3D\3D\3D\3E\3E\3E\3E\3E\3F\3F\3F"+ + "\3F\3F\3G\3G\3G\3G\3G\3G\3H\3H\3H\3H\3H\3I\3I\5I\u01e1\nI\3I\3I\3I\3J"+ + "\7J\u01e7\nJ\fJ\16J\u01ea\13J\3J\3J\7J\u01ee\nJ\fJ\16J\u01f1\13J\3J\3"+ + "J\5J\u01f5\nJ\3J\7J\u01f8\nJ\fJ\16J\u01fb\13J\3J\3J\7J\u01ff\nJ\fJ\16"+ + "J\u0202\13J\3J\3J\5J\u0206\nJ\3J\7J\u0209\nJ\fJ\16J\u020c\13J\7J\u020e"+ + "\nJ\fJ\16J\u0211\13J\3J\3J\3K\3K\7K\u0217\nK\fK\16K\u021a\13K\3L\3L\3"+ + "L\7L\u021f\nL\fL\16L\u0222\13L\5L\u0224\nL\3L\3L\3M\3M\7M\u022a\nM\fM"+ + "\16M\u022d\13M\3M\3M\5\u00a9\u00b3\u01c3\2N\4\3\6\4\b\5\n\6\f\7\16\b\20"+ + "\t\22\n\24\13\26\f\30\r\32\16\34\17\36\20 \21\"\22$\23&\24(\25*\26,\27"+ + ".\30\60\31\62\32\64\33\66\348\35:\36<\37> @!B\"D#F$H%J&L\'N(P)R*T+V,X"+ + "-Z.\\/^\60`\61b\62d\63f\64h\65j\66l\67n8p9r:t;v|?~@\u0080A\u0082"+ + "B\u0084C\u0086D\u0088E\u008aF\u008cG\u008eH\u0090I\u0092J\u0094\2\u0096"+ + "K\u0098L\u009aM\4\2\3\21\5\2\13\f\17\17\"\"\4\2\f\f\17\17\3\2\629\4\2"+ + "NNnn\4\2ZZzz\5\2\62;CHch\3\2\63;\3\2\62;\b\2FFHHNNffhhnn\4\2GGgg\4\2-"+ + "-//\4\2HHhh\4\2$$^^\5\2C\\aac|\6\2\62;C\\aac|\u024f\2\4\3\2\2\2\2\6\3"+ + "\2\2\2\2\b\3\2\2\2\2\n\3\2\2\2\2\f\3\2\2\2\2\16\3\2\2\2\2\20\3\2\2\2\2"+ + "\22\3\2\2\2\2\24\3\2\2\2\2\26\3\2\2\2\2\30\3\2\2\2\2\32\3\2\2\2\2\34\3"+ + "\2\2\2\2\36\3\2\2\2\2 \3\2\2\2\2\"\3\2\2\2\2$\3\2\2\2\2&\3\2\2\2\2(\3"+ + "\2\2\2\2*\3\2\2\2\2,\3\2\2\2\2.\3\2\2\2\2\60\3\2\2\2\2\62\3\2\2\2\2\64"+ + "\3\2\2\2\2\66\3\2\2\2\28\3\2\2\2\2:\3\2\2\2\2<\3\2\2\2\2>\3\2\2\2\2@\3"+ + "\2\2\2\2B\3\2\2\2\2D\3\2\2\2\2F\3\2\2\2\2H\3\2\2\2\2J\3\2\2\2\2L\3\2\2"+ + "\2\2N\3\2\2\2\2P\3\2\2\2\2R\3\2\2\2\2T\3\2\2\2\2V\3\2\2\2\2X\3\2\2\2\2"+ + "Z\3\2\2\2\2\\\3\2\2\2\2^\3\2\2\2\2`\3\2\2\2\2b\3\2\2\2\2d\3\2\2\2\2f\3"+ + "\2\2\2\2h\3\2\2\2\2j\3\2\2\2\2l\3\2\2\2\2n\3\2\2\2\2p\3\2\2\2\2r\3\2\2"+ + "\2\2t\3\2\2\2\2v\3\2\2\2\2x\3\2\2\2\2z\3\2\2\2\2|\3\2\2\2\2~\3\2\2\2\2"+ + "\u0080\3\2\2\2\2\u0082\3\2\2\2\2\u0084\3\2\2\2\2\u0086\3\2\2\2\2\u0088"+ + "\3\2\2\2\2\u008a\3\2\2\2\2\u008c\3\2\2\2\2\u008e\3\2\2\2\2\u0090\3\2\2"+ + "\2\2\u0092\3\2\2\2\2\u0096\3\2\2\2\3\u0098\3\2\2\2\3\u009a\3\2\2\2\4\u009d"+ + "\3\2\2\2\6\u00b8\3\2\2\2\b\u00bc\3\2\2\2\n\u00be\3\2\2\2\f\u00c0\3\2\2"+ + "\2\16\u00c2\3\2\2\2\20\u00c4\3\2\2\2\22\u00c6\3\2\2\2\24\u00c8\3\2\2\2"+ + "\26\u00cc\3\2\2\2\30\u00ce\3\2\2\2\32\u00d0\3\2\2\2\34\u00d3\3\2\2\2\36"+ + "\u00d8\3\2\2\2 \u00de\3\2\2\2\"\u00e1\3\2\2\2$\u00e5\3\2\2\2&\u00ee\3"+ + "\2\2\2(\u00f4\3\2\2\2*\u00fb\3\2\2\2,\u00ff\3\2\2\2.\u0103\3\2\2\2\60"+ + "\u0109\3\2\2\2\62\u010f\3\2\2\2\64\u0111\3\2\2\2\66\u0113\3\2\2\28\u0115"+ + "\3\2\2\2:\u0117\3\2\2\2<\u0119\3\2\2\2>\u011b\3\2\2\2@\u011d\3\2\2\2B"+ + "\u0120\3\2\2\2D\u0123\3\2\2\2F\u0127\3\2\2\2H\u0129\3\2\2\2J\u012c\3\2"+ + "\2\2L\u012e\3\2\2\2N\u0131\3\2\2\2P\u0134\3\2\2\2R\u0138\3\2\2\2T\u013b"+ + "\3\2\2\2V\u013f\3\2\2\2X\u0141\3\2\2\2Z\u0143\3\2\2\2\\\u0145\3\2\2\2"+ + "^\u0148\3\2\2\2`\u014b\3\2\2\2b\u014d\3\2\2\2d\u014f\3\2\2\2f\u0152\3"+ + "\2\2\2h\u0155\3\2\2\2j\u0157\3\2\2\2l\u015a\3\2\2\2n\u015d\3\2\2\2p\u0160"+ + "\3\2\2\2r\u0163\3\2\2\2t\u0166\3\2\2\2v\u0169\3\2\2\2x\u016c\3\2\2\2z"+ + "\u016f\3\2\2\2|\u0173\3\2\2\2~\u0177\3\2\2\2\u0080\u017c\3\2\2\2\u0082"+ + "\u0185\3\2\2\2\u0084\u0197\3\2\2\2\u0086\u01a4\3\2\2\2\u0088\u01bb\3\2"+ + "\2\2\u008a\u01c9\3\2\2\2\u008c\u01ce\3\2\2\2\u008e\u01d3\3\2\2\2\u0090"+ + "\u01d9\3\2\2\2\u0092\u01de\3\2\2\2\u0094\u01e8\3\2\2\2\u0096\u0214\3\2"+ + "\2\2\u0098\u0223\3\2\2\2\u009a\u0227\3\2\2\2\u009c\u009e\t\2\2\2\u009d"+ + "\u009c\3\2\2\2\u009e\u009f\3\2\2\2\u009f\u009d\3\2\2\2\u009f\u00a0\3\2"+ + "\2\2\u00a0\u00a1\3\2\2\2\u00a1\u00a2\b\2\2\2\u00a2\5\3\2\2\2\u00a3\u00a4"+ + "\7\61\2\2\u00a4\u00a5\7\61\2\2\u00a5\u00a9\3\2\2\2\u00a6\u00a8\13\2\2"+ + "\2\u00a7\u00a6\3\2\2\2\u00a8\u00ab\3\2\2\2\u00a9\u00aa\3\2\2\2\u00a9\u00a7"+ + "\3\2\2\2\u00aa\u00ac\3\2\2\2\u00ab\u00a9\3\2\2\2\u00ac\u00b9\t\3\2\2\u00ad"+ + "\u00ae\7\61\2\2\u00ae\u00af\7,\2\2\u00af\u00b3\3\2\2\2\u00b0\u00b2\13"+ + "\2\2\2\u00b1\u00b0\3\2\2\2\u00b2\u00b5\3\2\2\2\u00b3\u00b4\3\2\2\2\u00b3"+ + "\u00b1\3\2\2\2\u00b4\u00b6\3\2\2\2\u00b5\u00b3\3\2\2\2\u00b6\u00b7\7,"+ + "\2\2\u00b7\u00b9\7\61\2\2\u00b8\u00a3\3\2\2\2\u00b8\u00ad\3\2\2\2\u00b9"+ + "\u00ba\3\2\2\2\u00ba\u00bb\b\3\2\2\u00bb\7\3\2\2\2\u00bc\u00bd\7}\2\2"+ + "\u00bd\t\3\2\2\2\u00be\u00bf\7\177\2\2\u00bf\13\3\2\2\2\u00c0\u00c1\7"+ + "]\2\2\u00c1\r\3\2\2\2\u00c2\u00c3\7_\2\2\u00c3\17\3\2\2\2\u00c4\u00c5"+ + "\7*\2\2\u00c5\21\3\2\2\2\u00c6\u00c7\7+\2\2\u00c7\23\3\2\2\2\u00c8\u00c9"+ + "\7\60\2\2\u00c9\u00ca\3\2\2\2\u00ca\u00cb\b\n\3\2\u00cb\25\3\2\2\2\u00cc"+ + "\u00cd\7.\2\2\u00cd\27\3\2\2\2\u00ce\u00cf\7=\2\2\u00cf\31\3\2\2\2\u00d0"+ + "\u00d1\7k\2\2\u00d1\u00d2\7h\2\2\u00d2\33\3\2\2\2\u00d3\u00d4\7g\2\2\u00d4"+ + "\u00d5\7n\2\2\u00d5\u00d6\7u\2\2\u00d6\u00d7\7g\2\2\u00d7\35\3\2\2\2\u00d8"+ + "\u00d9\7y\2\2\u00d9\u00da\7j\2\2\u00da\u00db\7k\2\2\u00db\u00dc\7n\2\2"+ + "\u00dc\u00dd\7g\2\2\u00dd\37\3\2\2\2\u00de\u00df\7f\2\2\u00df\u00e0\7"+ + "q\2\2\u00e0!\3\2\2\2\u00e1\u00e2\7h\2\2\u00e2\u00e3\7q\2\2\u00e3\u00e4"+ + "\7t\2\2\u00e4#\3\2\2\2\u00e5\u00e6\7e\2\2\u00e6\u00e7\7q\2\2\u00e7\u00e8"+ + "\7p\2\2\u00e8\u00e9\7v\2\2\u00e9\u00ea\7k\2\2\u00ea\u00eb\7p\2\2\u00eb"+ + "\u00ec\7w\2\2\u00ec\u00ed\7g\2\2\u00ed%\3\2\2\2\u00ee\u00ef\7d\2\2\u00ef"+ + "\u00f0\7t\2\2\u00f0\u00f1\7g\2\2\u00f1\u00f2\7c\2\2\u00f2\u00f3\7m\2\2"+ + "\u00f3\'\3\2\2\2\u00f4\u00f5\7t\2\2\u00f5\u00f6\7g\2\2\u00f6\u00f7\7v"+ + "\2\2\u00f7\u00f8\7w\2\2\u00f8\u00f9\7t\2\2\u00f9\u00fa\7p\2\2\u00fa)\3"+ + "\2\2\2\u00fb\u00fc\7p\2\2\u00fc\u00fd\7g\2\2\u00fd\u00fe\7y\2\2\u00fe"+ + "+\3\2\2\2\u00ff\u0100\7v\2\2\u0100\u0101\7t\2\2\u0101\u0102\7{\2\2\u0102"+ + "-\3\2\2\2\u0103\u0104\7e\2\2\u0104\u0105\7c\2\2\u0105\u0106\7v\2\2\u0106"+ + "\u0107\7e\2\2\u0107\u0108\7j\2\2\u0108/\3\2\2\2\u0109\u010a\7v\2\2\u010a"+ + "\u010b\7j\2\2\u010b\u010c\7t\2\2\u010c\u010d\7q\2\2\u010d\u010e\7y\2\2"+ + "\u010e\61\3\2\2\2\u010f\u0110\7#\2\2\u0110\63\3\2\2\2\u0111\u0112\7\u0080"+ + "\2\2\u0112\65\3\2\2\2\u0113\u0114\7,\2\2\u0114\67\3\2\2\2\u0115\u0116"+ + "\7\61\2\2\u01169\3\2\2\2\u0117\u0118\7\'\2\2\u0118;\3\2\2\2\u0119\u011a"+ + "\7-\2\2\u011a=\3\2\2\2\u011b\u011c\7/\2\2\u011c?\3\2\2\2\u011d\u011e\7"+ + ">\2\2\u011e\u011f\7>\2\2\u011fA\3\2\2\2\u0120\u0121\7@\2\2\u0121\u0122"+ + "\7@\2\2\u0122C\3\2\2\2\u0123\u0124\7@\2\2\u0124\u0125\7@\2\2\u0125\u0126"+ + "\7@\2\2\u0126E\3\2\2\2\u0127\u0128\7>\2\2\u0128G\3\2\2\2\u0129\u012a\7"+ + ">\2\2\u012a\u012b\7?\2\2\u012bI\3\2\2\2\u012c\u012d\7@\2\2\u012dK\3\2"+ + "\2\2\u012e\u012f\7@\2\2\u012f\u0130\7?\2\2\u0130M\3\2\2\2\u0131\u0132"+ + "\7?\2\2\u0132\u0133\7?\2\2\u0133O\3\2\2\2\u0134\u0135\7?\2\2\u0135\u0136"+ + "\7?\2\2\u0136\u0137\7?\2\2\u0137Q\3\2\2\2\u0138\u0139\7#\2\2\u0139\u013a"+ + "\7?\2\2\u013aS\3\2\2\2\u013b\u013c\7#\2\2\u013c\u013d\7?\2\2\u013d\u013e"+ + "\7?\2\2\u013eU\3\2\2\2\u013f\u0140\7(\2\2\u0140W\3\2\2\2\u0141\u0142\7"+ + "`\2\2\u0142Y\3\2\2\2\u0143\u0144\7~\2\2\u0144[\3\2\2\2\u0145\u0146\7("+ + "\2\2\u0146\u0147\7(\2\2\u0147]\3\2\2\2\u0148\u0149\7~\2\2\u0149\u014a"+ + "\7~\2\2\u014a_\3\2\2\2\u014b\u014c\7A\2\2\u014ca\3\2\2\2\u014d\u014e\7"+ + "<\2\2\u014ec\3\2\2\2\u014f\u0150\7-\2\2\u0150\u0151\7-\2\2\u0151e\3\2"+ + "\2\2\u0152\u0153\7/\2\2\u0153\u0154\7/\2\2\u0154g\3\2\2\2\u0155\u0156"+ + "\7?\2\2\u0156i\3\2\2\2\u0157\u0158\7-\2\2\u0158\u0159\7?\2\2\u0159k\3"+ + "\2\2\2\u015a\u015b\7/\2\2\u015b\u015c\7?\2\2\u015cm\3\2\2\2\u015d\u015e"+ + "\7,\2\2\u015e\u015f\7?\2\2\u015fo\3\2\2\2\u0160\u0161\7\61\2\2\u0161\u0162"+ + "\7?\2\2\u0162q\3\2\2\2\u0163\u0164\7\'\2\2\u0164\u0165\7?\2\2\u0165s\3"+ + "\2\2\2\u0166\u0167\7(\2\2\u0167\u0168\7?\2\2\u0168u\3\2\2\2\u0169\u016a"+ + "\7`\2\2\u016a\u016b\7?\2\2\u016bw\3\2\2\2\u016c\u016d\7~\2\2\u016d\u016e"+ + "\7?\2\2\u016ey\3\2\2\2\u016f\u0170\7>\2\2\u0170\u0171\7>\2\2\u0171\u0172"+ + "\7?\2\2\u0172{\3\2\2\2\u0173\u0174\7@\2\2\u0174\u0175\7@\2\2\u0175\u0176"+ + "\7?\2\2\u0176}\3\2\2\2\u0177\u0178\7@\2\2\u0178\u0179\7@\2\2\u0179\u017a"+ + "\7@\2\2\u017a\u017b\7?\2\2\u017b\177\3\2\2\2\u017c\u017e\7\62\2\2\u017d"+ + "\u017f\t\4\2\2\u017e\u017d\3\2\2\2\u017f\u0180\3\2\2\2\u0180\u017e\3\2"+ + "\2\2\u0180\u0181\3\2\2\2\u0181\u0183\3\2\2\2\u0182\u0184\t\5\2\2\u0183"+ + "\u0182\3\2\2\2\u0183\u0184\3\2\2\2\u0184\u0081\3\2\2\2\u0185\u0186\7\62"+ + "\2\2\u0186\u0188\t\6\2\2\u0187\u0189\t\7\2\2\u0188\u0187\3\2\2\2\u0189"+ + "\u018a\3\2\2\2\u018a\u0188\3\2\2\2\u018a\u018b\3\2\2\2\u018b\u018d\3\2"+ + "\2\2\u018c\u018e\t\5\2\2\u018d\u018c\3\2\2\2\u018d\u018e\3\2\2\2\u018e"+ + "\u0083\3\2\2\2\u018f\u0198\7\62\2\2\u0190\u0194\t\b\2\2\u0191\u0193\t"+ + "\t\2\2\u0192\u0191\3\2\2\2\u0193\u0196\3\2\2\2\u0194\u0192\3\2\2\2\u0194"+ + "\u0195\3\2\2\2\u0195\u0198\3\2\2\2\u0196\u0194\3\2\2\2\u0197\u018f\3\2"+ + "\2\2\u0197\u0190\3\2\2\2\u0198\u019a\3\2\2\2\u0199\u019b\t\n\2\2\u019a"+ + "\u0199\3\2\2\2\u019a\u019b\3\2\2\2\u019b\u0085\3\2\2\2\u019c\u01a5\7\62"+ + "\2\2\u019d\u01a1\t\b\2\2\u019e\u01a0\t\t\2\2\u019f\u019e\3\2\2\2\u01a0"+ + "\u01a3\3\2\2\2\u01a1\u019f\3\2\2\2\u01a1\u01a2\3\2\2\2\u01a2\u01a5\3\2"+ + "\2\2\u01a3\u01a1\3\2\2\2\u01a4\u019c\3\2\2\2\u01a4\u019d\3\2\2\2\u01a5"+ + "\u01a6\3\2\2\2\u01a6\u01aa\5\24\n\2\u01a7\u01a9\t\t\2\2\u01a8\u01a7\3"+ + "\2\2\2\u01a9\u01ac\3\2\2\2\u01aa\u01a8\3\2\2\2\u01aa\u01ab\3\2\2\2\u01ab"+ + "\u01b6\3\2\2\2\u01ac\u01aa\3\2\2\2\u01ad\u01af\t\13\2\2\u01ae\u01b0\t"+ + "\f\2\2\u01af\u01ae\3\2\2\2\u01af\u01b0\3\2\2\2\u01b0\u01b2\3\2\2\2\u01b1"+ + "\u01b3\t\t\2\2\u01b2\u01b1\3\2\2\2\u01b3\u01b4\3\2\2\2\u01b4\u01b2\3\2"+ + "\2\2\u01b4\u01b5\3\2\2\2\u01b5\u01b7\3\2\2\2\u01b6\u01ad\3\2\2\2\u01b6"+ + "\u01b7\3\2\2\2\u01b7\u01b9\3\2\2\2\u01b8\u01ba\t\r\2\2\u01b9\u01b8\3\2"+ + "\2\2\u01b9\u01ba\3\2\2\2\u01ba\u0087\3\2\2\2\u01bb\u01c3\7$\2\2\u01bc"+ + "\u01bd\7^\2\2\u01bd\u01c2\7$\2\2\u01be\u01bf\7^\2\2\u01bf\u01c2\7^\2\2"+ + "\u01c0\u01c2\n\16\2\2\u01c1\u01bc\3\2\2\2\u01c1\u01be\3\2\2\2\u01c1\u01c0"+ + "\3\2\2\2\u01c2\u01c5\3\2\2\2\u01c3\u01c4\3\2\2\2\u01c3\u01c1\3\2\2\2\u01c4"+ + "\u01c6\3\2\2\2\u01c5\u01c3\3\2\2\2\u01c6\u01c7\7$\2\2\u01c7\u01c8\bD\4"+ + "\2\u01c8\u0089\3\2\2\2\u01c9\u01ca\7)\2\2\u01ca\u01cb\13\2\2\2\u01cb\u01cc"+ + "\7)\2\2\u01cc\u01cd\bE\5\2\u01cd\u008b\3\2\2\2\u01ce\u01cf\7v\2\2\u01cf"+ + "\u01d0\7t\2\2\u01d0\u01d1\7w\2\2\u01d1\u01d2\7g\2\2\u01d2\u008d\3\2\2"+ + "\2\u01d3\u01d4\7h\2\2\u01d4\u01d5\7c\2\2\u01d5\u01d6\7n\2\2\u01d6\u01d7"+ + "\7u\2\2\u01d7\u01d8\7g\2\2\u01d8\u008f\3\2\2\2\u01d9\u01da\7p\2\2\u01da"+ + "\u01db\7w\2\2\u01db\u01dc\7n\2\2\u01dc\u01dd\7n\2\2\u01dd\u0091\3\2\2"+ + "\2\u01de\u01e0\5\u0096K\2\u01df\u01e1\5\u0094J\2\u01e0\u01df\3\2\2\2\u01e0"+ + "\u01e1\3\2\2\2\u01e1\u01e2\3\2\2\2\u01e2\u01e3\6I\2\2\u01e3\u01e4\bI\6"+ + "\2\u01e4\u0093\3\2\2\2\u01e5\u01e7\7\"\2\2\u01e6\u01e5\3\2\2\2\u01e7\u01ea"+ + "\3\2\2\2\u01e8\u01e6\3\2\2\2\u01e8\u01e9\3\2\2\2\u01e9\u01eb\3\2\2\2\u01ea"+ + "\u01e8\3\2\2\2\u01eb\u01ef\7>\2\2\u01ec\u01ee\7\"\2\2\u01ed\u01ec\3\2"+ + "\2\2\u01ee\u01f1\3\2\2\2\u01ef\u01ed\3\2\2\2\u01ef\u01f0\3\2\2\2\u01f0"+ + "\u01f2\3\2\2\2\u01f1\u01ef\3\2\2\2\u01f2\u01f4\5\u0096K\2\u01f3\u01f5"+ + "\5\u0094J\2\u01f4\u01f3\3\2\2\2\u01f4\u01f5\3\2\2\2\u01f5\u01f9\3\2\2"+ + "\2\u01f6\u01f8\7\"\2\2\u01f7\u01f6\3\2\2\2\u01f8\u01fb\3\2\2\2\u01f9\u01f7"+ + "\3\2\2\2\u01f9\u01fa\3\2\2\2\u01fa\u020f\3\2\2\2\u01fb\u01f9\3\2\2\2\u01fc"+ + "\u0200\5\26\13\2\u01fd\u01ff\7\"\2\2\u01fe\u01fd\3\2\2\2\u01ff\u0202\3"+ + "\2\2\2\u0200\u01fe\3\2\2\2\u0200\u0201\3\2\2\2\u0201\u0203\3\2\2\2\u0202"+ + "\u0200\3\2\2\2\u0203\u0205\5\u0096K\2\u0204\u0206\5\u0094J\2\u0205\u0204"+ + "\3\2\2\2\u0205\u0206\3\2\2\2\u0206\u020a\3\2\2\2\u0207\u0209\7\"\2\2\u0208"+ + "\u0207\3\2\2\2\u0209\u020c\3\2\2\2\u020a\u0208\3\2\2\2\u020a\u020b\3\2"+ + "\2\2\u020b\u020e\3\2\2\2\u020c\u020a\3\2\2\2\u020d\u01fc\3\2\2\2\u020e"+ + "\u0211\3\2\2\2\u020f\u020d\3\2\2\2\u020f\u0210\3\2\2\2\u0210\u0212\3\2"+ + "\2\2\u0211\u020f\3\2\2\2\u0212\u0213\7@\2\2\u0213\u0095\3\2\2\2\u0214"+ + "\u0218\t\17\2\2\u0215\u0217\t\20\2\2\u0216\u0215\3\2\2\2\u0217\u021a\3"+ + "\2\2\2\u0218\u0216\3\2\2\2\u0218\u0219\3\2\2\2\u0219\u0097\3\2\2\2\u021a"+ + "\u0218\3\2\2\2\u021b\u0224\7\62\2\2\u021c\u0220\t\b\2\2\u021d\u021f\t"+ + "\t\2\2\u021e\u021d\3\2\2\2\u021f\u0222\3\2\2\2\u0220\u021e\3\2\2\2\u0220"+ + "\u0221\3\2\2\2\u0221\u0224\3\2\2\2\u0222\u0220\3\2\2\2\u0223\u021b\3\2"+ + "\2\2\u0223\u021c\3\2\2\2\u0224\u0225\3\2\2\2\u0225\u0226\bL\7\2\u0226"+ + "\u0099\3\2\2\2\u0227\u022b\t\17\2\2\u0228\u022a\t\20\2\2\u0229\u0228\3"+ + "\2\2\2\u022a\u022d\3\2\2\2\u022b\u0229\3\2\2\2\u022b\u022c\3\2\2\2\u022c"+ + "\u022e\3\2\2\2\u022d\u022b\3\2\2\2\u022e\u022f\bM\7\2\u022f\u009b\3\2"+ + "\2\2%\2\3\u009f\u00a9\u00b3\u00b8\u0180\u0183\u018a\u018d\u0194\u0197"+ + "\u019a\u01a1\u01a4\u01aa\u01af\u01b4\u01b6\u01b9\u01c1\u01c3\u01e0\u01e8"+ + "\u01ef\u01f4\u01f9\u0200\u0205\u020a\u020f\u0218\u0220\u0223\u022b\b\b"+ + "\2\2\4\3\2\3D\2\3E\3\3I\4\4\2\2"; public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); static { diff --git a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java index e7b331de661..3fd4b4075d5 100644 --- a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java +++ b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java @@ -1,25 +1,13 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless; - -import org.antlr.v4.runtime.FailedPredicateException; -import org.antlr.v4.runtime.NoViableAltException; -import org.antlr.v4.runtime.Parser; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.RuleContext; -import org.antlr.v4.runtime.RuntimeMetaData; -import org.antlr.v4.runtime.TokenStream; -import org.antlr.v4.runtime.Vocabulary; -import org.antlr.v4.runtime.VocabularyImpl; -import org.antlr.v4.runtime.atn.ATN; -import org.antlr.v4.runtime.atn.ATNDeserializer; -import org.antlr.v4.runtime.atn.ParserATNSimulator; -import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.atn.*; import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.tree.ParseTreeVisitor; -import org.antlr.v4.runtime.tree.TerminalNode; - +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.misc.*; +import org.antlr.v4.runtime.tree.*; import java.util.List; +import java.util.Iterator; +import java.util.ArrayList; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) class PainlessParser extends Parser { @@ -29,50 +17,49 @@ class PainlessParser extends Parser { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, - BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, - MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, - LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, - BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, - AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, - ALSH=60, ARSH=61, AUSH=62, ACAT=63, OCTAL=64, HEX=65, INTEGER=66, DECIMAL=67, - STRING=68, CHAR=69, TRUE=70, FALSE=71, NULL=72, TYPE=73, ID=74, EXTINTEGER=75, - EXTID=76; + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, + BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, + MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, + LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, + BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, + AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, + ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, + CHAR=68, TRUE=69, FALSE=70, NULL=71, TYPE=72, ID=73, EXTINTEGER=74, EXTID=75; public static final int - RULE_source = 0, RULE_statement = 1, RULE_block = 2, RULE_empty = 3, RULE_emptyscope = 4, - RULE_initializer = 5, RULE_afterthought = 6, RULE_declaration = 7, RULE_decltype = 8, - RULE_declvar = 9, RULE_trap = 10, RULE_expression = 11, RULE_extstart = 12, - RULE_extprec = 13, RULE_extcast = 14, RULE_extbrace = 15, RULE_extdot = 16, - RULE_exttype = 17, RULE_extcall = 18, RULE_extvar = 19, RULE_extfield = 20, + RULE_source = 0, RULE_statement = 1, RULE_block = 2, RULE_empty = 3, RULE_emptyscope = 4, + RULE_initializer = 5, RULE_afterthought = 6, RULE_declaration = 7, RULE_decltype = 8, + RULE_declvar = 9, RULE_trap = 10, RULE_expression = 11, RULE_extstart = 12, + RULE_extprec = 13, RULE_extcast = 14, RULE_extbrace = 15, RULE_extdot = 16, + RULE_exttype = 17, RULE_extcall = 18, RULE_extvar = 19, RULE_extfield = 20, RULE_extnew = 21, RULE_extstring = 22, RULE_arguments = 23, RULE_increment = 24; public static final String[] ruleNames = { - "source", "statement", "block", "empty", "emptyscope", "initializer", - "afterthought", "declaration", "decltype", "declvar", "trap", "expression", - "extstart", "extprec", "extcast", "extbrace", "extdot", "exttype", "extcall", + "source", "statement", "block", "empty", "emptyscope", "initializer", + "afterthought", "declaration", "decltype", "declvar", "trap", "expression", + "extstart", "extprec", "extcast", "extbrace", "extdot", "exttype", "extcall", "extvar", "extfield", "extnew", "extstring", "arguments", "increment" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", - "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", - "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", - "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", - "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", - "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", - "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", "'..='", null, - null, null, null, null, null, "'true'", "'false'", "'null'" + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", + "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", + "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", + "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", + "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", + "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", + "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, + null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "ACAT", - "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", - "NULL", "TYPE", "ID", "EXTINTEGER", "EXTID" + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + "TYPE", "ID", "EXTINTEGER", "EXTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @@ -149,7 +136,7 @@ class PainlessParser extends Parser { try { enterOuterAlt(_localctx, 1); { - setState(51); + setState(51); _errHandler.sync(this); _la = _input.LA(1); do { @@ -159,10 +146,10 @@ class PainlessParser extends Parser { statement(); } } - setState(53); + setState(53); _errHandler.sync(this); _la = _input.LA(1); - } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); + } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); setState(55); match(EOF); } @@ -183,7 +170,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_statement; } - + public StatementContext() { } public void copyFrom(StatementContext ctx) { super.copyFrom(ctx); @@ -469,7 +456,7 @@ class PainlessParser extends Parser { match(LP); setState(86); _la = _input.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { { setState(85); initializer(); @@ -480,7 +467,7 @@ class PainlessParser extends Parser { match(SEMICOLON); setState(90); _la = _input.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { { setState(89); expression(0); @@ -491,7 +478,7 @@ class PainlessParser extends Parser { match(SEMICOLON); setState(94); _la = _input.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { { setState(93); afterthought(); @@ -595,7 +582,7 @@ class PainlessParser extends Parser { match(TRY); setState(119); block(); - setState(121); + setState(121); _errHandler.sync(this); _alt = 1; do { @@ -611,7 +598,7 @@ class PainlessParser extends Parser { default: throw new NoViableAltException(this); } - setState(123); + setState(123); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,12,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); @@ -671,7 +658,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_block; } - + public BlockContext() { } public void copyFrom(BlockContext ctx) { super.copyFrom(ctx); @@ -718,7 +705,7 @@ class PainlessParser extends Parser { { setState(136); match(LBRACK); - setState(138); + setState(138); _errHandler.sync(this); _la = _input.LA(1); do { @@ -728,10 +715,10 @@ class PainlessParser extends Parser { statement(); } } - setState(140); + setState(140); _errHandler.sync(this); _la = _input.LA(1); - } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); + } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); setState(142); match(RBRACK); } @@ -1209,7 +1196,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_expression; } - + public ExpressionContext() { } public void copyFrom(ExpressionContext ctx) { super.copyFrom(ctx); @@ -1540,7 +1527,7 @@ class PainlessParser extends Parser { _prevctx = _localctx; setState(208); _la = _input.LA(1); - if ( !(((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)))) != 0)) ) { + if ( !(((((_la - 63)) & ~0x3f) == 0 && ((1L << (_la - 63)) & ((1L << (OCTAL - 63)) | (1L << (HEX - 63)) | (1L << (INTEGER - 63)) | (1L << (DECIMAL - 63)))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); @@ -1788,7 +1775,7 @@ class PainlessParser extends Parser { } break; } - } + } } setState(262); _errHandler.sync(this); @@ -2522,7 +2509,7 @@ class PainlessParser extends Parser { case LBRACE: { { - setState(338); + setState(338); _errHandler.sync(this); _alt = 1; do { @@ -2542,7 +2529,7 @@ class PainlessParser extends Parser { default: throw new NoViableAltException(this); } - setState(340); + setState(340); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,37,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); @@ -2665,7 +2652,7 @@ class PainlessParser extends Parser { match(LP); setState(361); _la = _input.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (OCTAL - 64)) | (1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0)) { { setState(353); expression(0); @@ -2781,7 +2768,7 @@ class PainlessParser extends Parser { } public static final String _serializedATN = - "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\3N\u0172\4\2\t\2\4"+ + "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\3M\u0172\4\2\t\2\4"+ "\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13\t"+ "\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22\t\22"+ "\4\23\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30\4\31\t\31"+ @@ -2809,8 +2796,8 @@ class PainlessParser extends Parser { "\27\5\27\u015a\n\27\5\27\u015c\n\27\3\30\3\30\3\30\5\30\u0161\n\30\3\31"+ "\3\31\3\31\3\31\7\31\u0167\n\31\f\31\16\31\u016a\13\31\5\31\u016c\n\31"+ "\3\31\3\31\3\32\3\32\3\32\2\3\30\33\2\4\6\b\n\f\16\20\22\24\26\30\32\34"+ - "\36 \"$&(*,.\60\62\2\f\4\2\32\33\37 \3\2\65@\3\2BE\3\2\34\36\3\2\37 \3"+ - "\2!#\3\2$\'\3\2(+\3\2MN\3\2\63\64\u01b2\2\65\3\2\2\2\4\u0088\3\2\2\2\6"+ + "\36 \"$&(*,.\60\62\2\f\4\2\32\33\37 \3\2\65@\3\2AD\3\2\34\36\3\2\37 \3"+ + "\2!#\3\2$\'\3\2(+\3\2LM\3\2\63\64\u01b2\2\65\3\2\2\2\4\u0088\3\2\2\2\6"+ "\u0093\3\2\2\2\b\u0097\3\2\2\2\n\u0099\3\2\2\2\f\u009e\3\2\2\2\16\u00a0"+ "\3\2\2\2\20\u00a2\3\2\2\2\22\u00ab\3\2\2\2\24\u00b3\3\2\2\2\26\u00b8\3"+ "\2\2\2\30\u00de\3\2\2\2\32\u010f\3\2\2\2\34\u0111\3\2\2\2\36\u011f\3\2"+ @@ -2846,20 +2833,20 @@ class PainlessParser extends Parser { "\r\3\2\2\2\u00a0\u00a1\5\30\r\2\u00a1\17\3\2\2\2\u00a2\u00a3\5\22\n\2"+ "\u00a3\u00a8\5\24\13\2\u00a4\u00a5\7\f\2\2\u00a5\u00a7\5\24\13\2\u00a6"+ "\u00a4\3\2\2\2\u00a7\u00aa\3\2\2\2\u00a8\u00a6\3\2\2\2\u00a8\u00a9\3\2"+ - "\2\2\u00a9\21\3\2\2\2\u00aa\u00a8\3\2\2\2\u00ab\u00b0\7K\2\2\u00ac\u00ad"+ + "\2\2\u00a9\21\3\2\2\2\u00aa\u00a8\3\2\2\2\u00ab\u00b0\7J\2\2\u00ac\u00ad"+ "\7\7\2\2\u00ad\u00af\7\b\2\2\u00ae\u00ac\3\2\2\2\u00af\u00b2\3\2\2\2\u00b0"+ "\u00ae\3\2\2\2\u00b0\u00b1\3\2\2\2\u00b1\23\3\2\2\2\u00b2\u00b0\3\2\2"+ - "\2\u00b3\u00b6\7L\2\2\u00b4\u00b5\7\65\2\2\u00b5\u00b7\5\30\r\2\u00b6"+ + "\2\u00b3\u00b6\7K\2\2\u00b4\u00b5\7\65\2\2\u00b5\u00b7\5\30\r\2\u00b6"+ "\u00b4\3\2\2\2\u00b6\u00b7\3\2\2\2\u00b7\25\3\2\2\2\u00b8\u00b9\7\30\2"+ - "\2\u00b9\u00ba\7\t\2\2\u00ba\u00bb\7K\2\2\u00bb\u00bc\7L\2\2\u00bc\u00bd"+ + "\2\u00b9\u00ba\7\t\2\2\u00ba\u00bb\7J\2\2\u00bb\u00bc\7K\2\2\u00bc\u00bd"+ "\3\2\2\2\u00bd\u00c0\7\n\2\2\u00be\u00c1\5\6\4\2\u00bf\u00c1\5\n\6\2\u00c0"+ "\u00be\3\2\2\2\u00c0\u00bf\3\2\2\2\u00c1\27\3\2\2\2\u00c2\u00c3\b\r\1"+ "\2\u00c3\u00c4\t\2\2\2\u00c4\u00df\5\30\r\20\u00c5\u00c6\7\t\2\2\u00c6"+ "\u00c7\5\22\n\2\u00c7\u00c8\7\n\2\2\u00c8\u00c9\5\30\r\17\u00c9\u00df"+ "\3\2\2\2\u00ca\u00cb\5\32\16\2\u00cb\u00cc\t\3\2\2\u00cc\u00cd\5\30\r"+ "\3\u00cd\u00df\3\2\2\2\u00ce\u00cf\7\t\2\2\u00cf\u00d0\5\30\r\2\u00d0"+ - "\u00d1\7\n\2\2\u00d1\u00df\3\2\2\2\u00d2\u00df\t\4\2\2\u00d3\u00df\7G"+ - "\2\2\u00d4\u00df\7H\2\2\u00d5\u00df\7I\2\2\u00d6\u00df\7J\2\2\u00d7\u00d8"+ + "\u00d1\7\n\2\2\u00d1\u00df\3\2\2\2\u00d2\u00df\t\4\2\2\u00d3\u00df\7F"+ + "\2\2\u00d4\u00df\7G\2\2\u00d5\u00df\7H\2\2\u00d6\u00df\7I\2\2\u00d7\u00d8"+ "\5\32\16\2\u00d8\u00d9\5\62\32\2\u00d9\u00df\3\2\2\2\u00da\u00db\5\62"+ "\32\2\u00db\u00dc\5\32\16\2\u00dc\u00df\3\2\2\2\u00dd\u00df\5\32\16\2"+ "\u00de\u00c2\3\2\2\2\u00de\u00c5\3\2\2\2\u00de\u00ca\3\2\2\2\u00de\u00ce"+ @@ -2899,21 +2886,21 @@ class PainlessParser extends Parser { "\u0130\5 \21\2\u012f\u012d\3\2\2\2\u012f\u012e\3\2\2\2\u012f\u0130\3\2"+ "\2\2\u0130!\3\2\2\2\u0131\u0134\7\13\2\2\u0132\u0135\5&\24\2\u0133\u0135"+ "\5*\26\2\u0134\u0132\3\2\2\2\u0134\u0133\3\2\2\2\u0135#\3\2\2\2\u0136"+ - "\u0137\7K\2\2\u0137\u0138\5\"\22\2\u0138%\3\2\2\2\u0139\u013a\7N\2\2\u013a"+ + "\u0137\7J\2\2\u0137\u0138\5\"\22\2\u0138%\3\2\2\2\u0139\u013a\7M\2\2\u013a"+ "\u013d\5\60\31\2\u013b\u013e\5\"\22\2\u013c\u013e\5 \21\2\u013d\u013b"+ "\3\2\2\2\u013d\u013c\3\2\2\2\u013d\u013e\3\2\2\2\u013e\'\3\2\2\2\u013f"+ - "\u0142\7L\2\2\u0140\u0143\5\"\22\2\u0141\u0143\5 \21\2\u0142\u0140\3\2"+ + "\u0142\7K\2\2\u0140\u0143\5\"\22\2\u0141\u0143\5 \21\2\u0142\u0140\3\2"+ "\2\2\u0142\u0141\3\2\2\2\u0142\u0143\3\2\2\2\u0143)\3\2\2\2\u0144\u0147"+ "\t\n\2\2\u0145\u0148\5\"\22\2\u0146\u0148\5 \21\2\u0147\u0145\3\2\2\2"+ "\u0147\u0146\3\2\2\2\u0147\u0148\3\2\2\2\u0148+\3\2\2\2\u0149\u014a\7"+ - "\26\2\2\u014a\u015b\7K\2\2\u014b\u014e\5\60\31\2\u014c\u014f\5\"\22\2"+ + "\26\2\2\u014a\u015b\7J\2\2\u014b\u014e\5\60\31\2\u014c\u014f\5\"\22\2"+ "\u014d\u014f\5 \21\2\u014e\u014c\3\2\2\2\u014e\u014d\3\2\2\2\u014e\u014f"+ "\3\2\2\2\u014f\u015c\3\2\2\2\u0150\u0151\7\7\2\2\u0151\u0152\5\30\r\2"+ "\u0152\u0153\7\b\2\2\u0153\u0155\3\2\2\2\u0154\u0150\3\2\2\2\u0155\u0156"+ "\3\2\2\2\u0156\u0154\3\2\2\2\u0156\u0157\3\2\2\2\u0157\u0159\3\2\2\2\u0158"+ "\u015a\5\"\22\2\u0159\u0158\3\2\2\2\u0159\u015a\3\2\2\2\u015a\u015c\3"+ "\2\2\2\u015b\u014b\3\2\2\2\u015b\u0154\3\2\2\2\u015c-\3\2\2\2\u015d\u0160"+ - "\7F\2\2\u015e\u0161\5\"\22\2\u015f\u0161\5 \21\2\u0160\u015e\3\2\2\2\u0160"+ + "\7E\2\2\u015e\u0161\5\"\22\2\u015f\u0161\5 \21\2\u0160\u015e\3\2\2\2\u0160"+ "\u015f\3\2\2\2\u0160\u0161\3\2\2\2\u0161/\3\2\2\2\u0162\u016b\7\t\2\2"+ "\u0163\u0168\5\30\r\2\u0164\u0165\7\f\2\2\u0165\u0167\5\30\r\2\u0166\u0164"+ "\3\2\2\2\u0167\u016a\3\2\2\2\u0168\u0166\3\2\2\2\u0168\u0169\3\2\2\2\u0169"+ diff --git a/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java b/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java index b4807bb5b4c..b589f9765cd 100644 --- a/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java +++ b/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java @@ -46,7 +46,7 @@ public class NoSemiColonTest extends ScriptTestCase { public void testWhileStatement() { - assertEquals("aaaaaa", exec("String c = \"a\" int x while (x < 5) { c ..= \"a\" ++x } return c")); + assertEquals("aaaaaa", exec("String c = \"a\" int x while (x < 5) { ++x c += \"a\" } return c")); Object value = exec( " byte[][] b = new byte[5][5] \n" + @@ -75,24 +75,24 @@ public class NoSemiColonTest extends ScriptTestCase { } public void testDoWhileStatement() { - assertEquals("aaaaaa", exec("String c = \"a\" int x do { c ..= \"a\" ++x } while (x < 5) return c")); + assertEquals("aaaaaa", exec("String c = \"a\" int x do { c += \"a\"; ++x } while (x < 5) return c")); Object value = exec( - " long[][] l = new long[5][5] \n" + - " long x = 0, y \n" + - " \n" + - " do { \n" + - " y = 0 \n" + - " \n" + - " do { \n" + - " l[(int)x][(int)y] = x*y \n" + - " ++y \n" + - " } while (y < 5) \n" + - " \n" + - " ++x \n" + - " } while (x < 5) \n" + - " \n" + - " return l \n"); + " long[][] l = new long[5][5] \n" + + " long x = 0, y \n" + + " \n" + + " do { \n" + + " y = 0 \n" + + " \n" + + " do { \n" + + " l[(int)x][(int)y] = x*y; \n" + + " ++y \n" + + " } while (y < 5) \n" + + " \n" + + " ++x \n" + + " } while (x < 5) \n" + + " \n" + + " return l \n"); long[][] l = (long[][])value; @@ -104,7 +104,7 @@ public class NoSemiColonTest extends ScriptTestCase { } public void testForStatement() { - assertEquals("aaaaaa", exec("String c = \"a\" for (int x = 0; x < 5; ++x) c ..= \"a\" return c")); + assertEquals("aaaaaa", exec("String c = \"a\" for (int x = 0; x < 5; ++x) c += \"a\" return c")); Object value = exec( " int[][] i = new int[5][5] \n" + From a2c37c09897eead58b2d37e36a203af8aa84c687 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 1 Feb 2016 16:18:20 -0800 Subject: [PATCH 13/49] CliTool: Allow unexpected exceptions to propagate Cli tools currently catch all exceptions, and only print the exception message, except when a special system property is set. Even with this flag set, certain exceptions, like IOException, are captured and their stack trace is always lost. This change adds a UserError class, which can be used a cli tools to specify a message to the user, as well as an exit status. All other exceptions are propagated out of main, so java will exit with non-zero and print the stack trace. --- .../bootstrap/BootstrapCLIParser.java | 11 +-- .../org/elasticsearch/common/cli/CliTool.java | 49 ++++++------- .../elasticsearch/common/cli/Terminal.java | 11 --- .../elasticsearch/common/cli/UserError.java | 35 ++++++++++ .../org/elasticsearch/env/Environment.java | 27 -------- .../plugins/InstallPluginCommand.java | 29 ++++---- .../org/elasticsearch/plugins/PluginCli.java | 2 +- .../plugins/RemovePluginCommand.java | 5 +- .../common/cli/TerminalTests.java | 18 ----- .../elasticsearch/plugins/PluginCliTests.java | 16 +---- .../mapper/attachments/StandaloneRunner.java | 14 ++-- .../bootstrap/BootstrapCliParserTests.java | 13 ++-- .../common/cli/CliToolTests.java | 68 ++++--------------- .../plugins/InstallPluginCommandTests.java | 28 +++++--- .../plugins/RemovePluginCommandTests.java | 6 +- 15 files changed, 125 insertions(+), 207 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/common/cli/UserError.java diff --git a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java index 51a16f104bc..c89ca8ea3dd 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.CliToolConfig; import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.monitor.jvm.JvmInfo; @@ -103,7 +104,7 @@ final class BootstrapCLIParser extends CliTool { // TODO: don't use system properties as a way to do this, its horrible... @SuppressForbidden(reason = "Sets system properties passed as CLI parameters") - public static Command parse(Terminal terminal, CommandLine cli) { + public static Command parse(Terminal terminal, CommandLine cli) throws UserError { if (cli.hasOption("V")) { return Version.parse(terminal, cli); } @@ -132,11 +133,11 @@ final class BootstrapCLIParser extends CliTool { String arg = iterator.next(); if (!arg.startsWith("--")) { if (arg.startsWith("-D") || arg.startsWith("-d") || arg.startsWith("-p")) { - throw new IllegalArgumentException( + throw new UserError(ExitStatus.USAGE, "Parameter [" + arg + "] starting with \"-D\", \"-d\" or \"-p\" must be before any parameters starting with --" ); } else { - throw new IllegalArgumentException("Parameter [" + arg + "]does not start with --"); + throw new UserError(ExitStatus.USAGE, "Parameter [" + arg + "]does not start with --"); } } // if there is no = sign, we have to get the next argu @@ -150,11 +151,11 @@ final class BootstrapCLIParser extends CliTool { if (iterator.hasNext()) { String value = iterator.next(); if (value.startsWith("--")) { - throw new IllegalArgumentException("Parameter [" + arg + "] needs value"); + throw new UserError(ExitStatus.USAGE, "Parameter [" + arg + "] needs value"); } System.setProperty("es." + arg, value); } else { - throw new IllegalArgumentException("Parameter [" + arg + "] needs value"); + throw new UserError(ExitStatus.USAGE, "Parameter [" + arg + "] needs value"); } } } diff --git a/core/src/main/java/org/elasticsearch/common/cli/CliTool.java b/core/src/main/java/org/elasticsearch/common/cli/CliTool.java index 0d32b6e2779..7e954bc85ee 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/CliTool.java +++ b/core/src/main/java/org/elasticsearch/common/cli/CliTool.java @@ -19,9 +19,14 @@ package org.elasticsearch.common.cli; +import org.apache.commons.cli.AlreadySelectedException; +import org.apache.commons.cli.AmbiguousOptionException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.MissingArgumentException; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.UnrecognizedOptionException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.node.internal.InternalSettingsPreparer; @@ -50,7 +55,7 @@ import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; public abstract class CliTool { // based on sysexits.h - public static enum ExitStatus { + public enum ExitStatus { OK(0), OK_AND_EXIT(0), USAGE(64), /* command line usage error */ @@ -69,23 +74,13 @@ public abstract class CliTool { final int status; - private ExitStatus(int status) { + ExitStatus(int status) { this.status = status; } public int status() { return status; } - - public static ExitStatus fromStatus(int status) { - for (ExitStatus exitStatus : values()) { - if (exitStatus.status() == status) { - return exitStatus; - } - } - - return null; - } } protected final Terminal terminal; @@ -108,7 +103,7 @@ public abstract class CliTool { settings = env.settings(); } - public final ExitStatus execute(String... args) { + public final ExitStatus execute(String... args) throws Exception { // first lets see if the user requests tool help. We're doing it only if // this is a multi-command tool. If it's a single command tool, the -h/--help @@ -146,23 +141,11 @@ public abstract class CliTool { } } - Command command = null; try { - - command = parse(cmd, args); - return command.execute(settings, env); - } catch (IOException ioe) { - terminal.printError(ioe); - return ExitStatus.IO_ERROR; - } catch (IllegalArgumentException ilae) { - terminal.printError(ilae); - return ExitStatus.USAGE; - } catch (Throwable t) { - terminal.printError(t); - if (command == null) { - return ExitStatus.USAGE; - } - return ExitStatus.CODE_ERROR; + return parse(cmd, args).execute(settings, env); + } catch (UserError error) { + terminal.printError(error.getMessage()); + return error.exitStatus; } } @@ -177,7 +160,13 @@ public abstract class CliTool { if (cli.hasOption("h")) { return helpCmd(cmd); } - cli = parser.parse(cmd.options(), args, cmd.isStopAtNonOption()); + try { + cli = parser.parse(cmd.options(), args, cmd.isStopAtNonOption()); + } catch (AlreadySelectedException|MissingArgumentException|MissingOptionException|UnrecognizedOptionException e) { + // intentionally drop the stack trace here as these are really user errors, + // the stack trace into cli parsing lib is not important + throw new UserError(ExitStatus.USAGE, e.toString()); + } Terminal.Verbosity verbosity = Terminal.Verbosity.resolve(cli); terminal.verbosity(verbosity); return parse(cmd.name(), cli); diff --git a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java index 5e4bc09ad9f..7608c9812de 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java +++ b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java @@ -35,8 +35,6 @@ import java.util.Locale; @SuppressForbidden(reason = "System#out") public abstract class Terminal { - public static final String DEBUG_SYSTEM_PROPERTY = "es.cli.debug"; - public static final Terminal DEFAULT = ConsoleTerminal.supported() ? new ConsoleTerminal() : new SystemTerminal(); public static enum Verbosity { @@ -64,7 +62,6 @@ public abstract class Terminal { } private Verbosity verbosity = Verbosity.NORMAL; - private final boolean isDebugEnabled; public Terminal() { this(Verbosity.NORMAL); @@ -72,7 +69,6 @@ public abstract class Terminal { public Terminal(Verbosity verbosity) { this.verbosity = verbosity; - this.isDebugEnabled = "true".equals(System.getProperty(DEBUG_SYSTEM_PROPERTY, "false")); } public void verbosity(Verbosity verbosity) { @@ -119,13 +115,6 @@ public abstract class Terminal { println(Verbosity.SILENT, "ERROR: " + msg, args); } - public void printError(Throwable t) { - printError("%s", t.toString()); - if (isDebugEnabled) { - printStackTrace(t); - } - } - public void printWarn(String msg, Object... args) { println(Verbosity.SILENT, "WARN: " + msg, args); } diff --git a/core/src/main/java/org/elasticsearch/common/cli/UserError.java b/core/src/main/java/org/elasticsearch/common/cli/UserError.java new file mode 100644 index 00000000000..ad709830885 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/cli/UserError.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.cli; + +/** + * An exception representing a user fixable problem in {@link CliTool} usage. + */ +public class UserError extends Exception { + + /** The exist status the cli should use when catching this user error. */ + public final CliTool.ExitStatus exitStatus; + + /** Constructs a UserError with an exit status and message to show the user. */ + public UserError(CliTool.ExitStatus exitStatus, String msg) { + super(msg); + this.exitStatus = exitStatus; + } +} diff --git a/core/src/main/java/org/elasticsearch/env/Environment.java b/core/src/main/java/org/elasticsearch/env/Environment.java index 65d62bd9e33..2fc85099e8b 100644 --- a/core/src/main/java/org/elasticsearch/env/Environment.java +++ b/core/src/main/java/org/elasticsearch/env/Environment.java @@ -330,31 +330,4 @@ public class Environment { public static FileStore getFileStore(Path path) throws IOException { return ESFileStore.getMatchingFileStore(path, fileStores); } - - /** - * Returns true if the path is writable. - * Acts just like {@link Files#isWritable(Path)}, except won't - * falsely return false for paths on SUBST'd drive letters - * See https://bugs.openjdk.java.net/browse/JDK-8034057 - * Note this will set the file modification time (to its already-set value) - * to test access. - */ - @SuppressForbidden(reason = "works around https://bugs.openjdk.java.net/browse/JDK-8034057") - public static boolean isWritable(Path path) throws IOException { - boolean v = Files.isWritable(path); - if (v || Constants.WINDOWS == false) { - return v; - } - - // isWritable returned false on windows, the hack begins!!!!!! - // resetting the modification time is the least destructive/simplest - // way to check for both files and directories, and fails early just - // in getting the current value if file doesn't exist, etc - try { - Files.setLastModifiedTime(path, Files.getLastModifiedTime(path)); - return true; - } catch (Throwable e) { - return false; - } - } } diff --git a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 656378b0f89..5600d407948 100644 --- a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -48,6 +48,7 @@ import org.elasticsearch.Version; import org.elasticsearch.bootstrap.JarHell; import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.settings.Settings; @@ -136,10 +137,6 @@ class InstallPluginCommand extends CliTool.Command { Files.createDirectory(env.pluginsFile()); } - if (Environment.isWritable(env.pluginsFile()) == false) { - throw new IOException("Plugins directory is read only: " + env.pluginsFile()); - } - Path pluginZip = download(pluginId, env.tmpFile()); Path extractedZip = unzip(pluginZip, env.pluginsFile()); install(extractedZip, env); @@ -148,7 +145,7 @@ class InstallPluginCommand extends CliTool.Command { } /** Downloads the plugin and returns the file it was downloaded to. */ - private Path download(String pluginId, Path tmpDir) throws IOException { + private Path download(String pluginId, Path tmpDir) throws Exception { if (OFFICIAL_PLUGINS.contains(pluginId)) { final String version = Version.CURRENT.toString(); final String url; @@ -189,7 +186,7 @@ class InstallPluginCommand extends CliTool.Command { } /** Downloads a zip from the url, as well as a SHA1 checksum, and checks the checksum. */ - private Path downloadZipAndChecksum(String urlString, Path tmpDir) throws IOException { + private Path downloadZipAndChecksum(String urlString, Path tmpDir) throws Exception { Path zip = downloadZip(urlString, tmpDir); URL checksumUrl = new URL(urlString + ".sha1"); @@ -198,14 +195,14 @@ class InstallPluginCommand extends CliTool.Command { BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); expectedChecksum = checksumReader.readLine(); if (checksumReader.readLine() != null) { - throw new IllegalArgumentException("Invalid checksum file at " + urlString.toString()); + throw new UserError(CliTool.ExitStatus.IO_ERROR, "Invalid checksum file at " + checksumUrl); } } byte[] zipbytes = Files.readAllBytes(zip); String gotChecksum = MessageDigests.toHexString(MessageDigests.sha1().digest(zipbytes)); if (expectedChecksum.equals(gotChecksum) == false) { - throw new IllegalStateException("SHA1 mismatch, expected " + expectedChecksum + " but got " + gotChecksum); + throw new UserError(CliTool.ExitStatus.IO_ERROR, "SHA1 mismatch, expected " + expectedChecksum + " but got " + gotChecksum); } return zip; @@ -250,7 +247,7 @@ class InstallPluginCommand extends CliTool.Command { // don't let luser install plugin as a module... // they might be unavoidably in maven central and are packaged up the same way) if (MODULES.contains(info.getName())) { - throw new IOException("plugin '" + info.getName() + "' cannot be installed like this, it is a system module"); + throw new UserError(CliTool.ExitStatus.USAGE, "plugin '" + info.getName() + "' cannot be installed like this, it is a system module"); } // check for jar hell before any copying @@ -306,7 +303,7 @@ class InstallPluginCommand extends CliTool.Command { final Path destination = env.pluginsFile().resolve(info.getName()); if (Files.exists(destination)) { - throw new IOException("plugin directory " + destination.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + info.getName() + "' command"); + throw new UserError(CliTool.ExitStatus.USAGE, "plugin directory " + destination.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + info.getName() + "' command"); } Path tmpBinDir = tmpRoot.resolve("bin"); @@ -337,9 +334,9 @@ class InstallPluginCommand extends CliTool.Command { } /** Copies the files from {@code tmpBinDir} into {@code destBinDir}, along with permissions from dest dirs parent. */ - private void installBin(PluginInfo info, Path tmpBinDir, Path destBinDir) throws IOException { + private void installBin(PluginInfo info, Path tmpBinDir, Path destBinDir) throws Exception { if (Files.isDirectory(tmpBinDir) == false) { - throw new IOException("bin in plugin " + info.getName() + " is not a directory"); + throw new UserError(CliTool.ExitStatus.IO_ERROR, "bin in plugin " + info.getName() + " is not a directory"); } Files.createDirectory(destBinDir); @@ -357,7 +354,7 @@ class InstallPluginCommand extends CliTool.Command { try (DirectoryStream stream = Files.newDirectoryStream(tmpBinDir)) { for (Path srcFile : stream) { if (Files.isDirectory(srcFile)) { - throw new IOException("Directories not allowed in bin dir for plugin " + info.getName()); + throw new UserError(CliTool.ExitStatus.DATA_ERROR, "Directories not allowed in bin dir for plugin " + info.getName() + ", found " + srcFile.getFileName()); } Path destFile = destBinDir.resolve(tmpBinDir.relativize(srcFile)); @@ -376,9 +373,9 @@ class InstallPluginCommand extends CliTool.Command { * Copies the files from {@code tmpConfigDir} into {@code destConfigDir}. * Any files existing in both the source and destination will be skipped. */ - private void installConfig(PluginInfo info, Path tmpConfigDir, Path destConfigDir) throws IOException { + private void installConfig(PluginInfo info, Path tmpConfigDir, Path destConfigDir) throws Exception { if (Files.isDirectory(tmpConfigDir) == false) { - throw new IOException("config in plugin " + info.getName() + " is not a directory"); + throw new UserError(CliTool.ExitStatus.IO_ERROR, "config in plugin " + info.getName() + " is not a directory"); } // create the plugin's config dir "if necessary" @@ -387,7 +384,7 @@ class InstallPluginCommand extends CliTool.Command { try (DirectoryStream stream = Files.newDirectoryStream(tmpConfigDir)) { for (Path srcFile : stream) { if (Files.isDirectory(srcFile)) { - throw new IOException("Directories not allowed in config dir for plugin " + info.getName()); + throw new UserError(CliTool.ExitStatus.DATA_ERROR, "Directories not allowed in config dir for plugin " + info.getName()); } Path destFile = destConfigDir.resolve(tmpConfigDir.relativize(srcFile)); diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginCli.java b/core/src/main/java/org/elasticsearch/plugins/PluginCli.java index c69c07b3d6b..30a36501a61 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginCli.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginCli.java @@ -55,7 +55,7 @@ public class PluginCli extends CliTool { .cmds(LIST_CMD, INSTALL_CMD, REMOVE_CMD) .build(); - public static void main(String[] args) { + public static void main(String[] args) throws Exception { // initialize default for es.logger.level because we will not read the logging.yml String loggerLevel = System.getProperty("es.logger.level", "INFO"); // Set the appender for all potential log files to terminal so that other components that use the logger print out the diff --git a/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java index f5e55ba2b24..d8fd0b8f250 100644 --- a/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java @@ -29,6 +29,7 @@ import org.apache.lucene.util.IOUtils; import org.elasticsearch.common.Strings; import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; @@ -51,7 +52,7 @@ class RemovePluginCommand extends CliTool.Command { Path pluginDir = env.pluginsFile().resolve(pluginName); if (Files.exists(pluginDir) == false) { - throw new IllegalArgumentException("Plugin " + pluginName + " not found. Run 'plugin list' to get list of installed plugins."); + throw new UserError(CliTool.ExitStatus.USAGE, "Plugin " + pluginName + " not found. Run 'plugin list' to get list of installed plugins."); } List pluginPaths = new ArrayList<>(); @@ -59,7 +60,7 @@ class RemovePluginCommand extends CliTool.Command { Path pluginBinDir = env.binFile().resolve(pluginName); if (Files.exists(pluginBinDir)) { if (Files.isDirectory(pluginBinDir) == false) { - throw new IllegalStateException("Bin dir for " + pluginName + " is not a directory"); + throw new UserError(CliTool.ExitStatus.IO_ERROR, "Bin dir for " + pluginName + " is not a directory"); } pluginPaths.add(pluginBinDir); terminal.println(VERBOSE, "Removing: %s", pluginBinDir); diff --git a/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java b/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java index 259ee109f0f..498dd269a23 100644 --- a/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java +++ b/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java @@ -46,24 +46,6 @@ public class TerminalTests extends CliToolTestCase { assertPrinted(terminal, Terminal.Verbosity.VERBOSE, "text"); } - public void testError() throws Exception { - try { - // actually throw so we have a stacktrace - throw new NoSuchFileException("/path/to/some/file"); - } catch (NoSuchFileException e) { - CaptureOutputTerminal terminal = new CaptureOutputTerminal(Terminal.Verbosity.NORMAL); - terminal.printError(e); - List output = terminal.getTerminalOutput(); - assertFalse(output.isEmpty()); - assertTrue(output.get(0), output.get(0).contains("NoSuchFileException")); // exception class - assertTrue(output.get(0), output.get(0).contains("/path/to/some/file")); // message - assertEquals(1, output.size()); - - // TODO: we should test stack trace is printed in debug mode...except debug is a sysprop instead of - // a command line param...maybe it should be VERBOSE instead of a separate debug prop? - } - } - private void assertPrinted(CaptureOutputTerminal logTerminal, Terminal.Verbosity verbosity, String text) { logTerminal.print(verbosity, text); assertThat(logTerminal.getTerminalOutput(), hasSize(1)); diff --git a/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java index 99d1c33821b..3a121590083 100644 --- a/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginCliTests.java @@ -19,21 +19,15 @@ package org.elasticsearch.plugins; -import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.CliToolTestCase; -import java.io.IOException; -import java.net.MalformedURLException; -import java.nio.file.Path; - -import static org.elasticsearch.common.cli.CliTool.ExitStatus.IO_ERROR; import static org.elasticsearch.common.cli.CliTool.ExitStatus.OK_AND_EXIT; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; public class PluginCliTests extends CliToolTestCase { - public void testHelpWorks() throws IOException { + public void testHelpWorks() throws Exception { CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(); assertThat(new PluginCli(terminal).execute(args("--help")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin.help"); @@ -53,12 +47,4 @@ public class PluginCliTests extends CliToolTestCase { assertThat(new PluginCli(terminal).execute(args("list -h")), is(OK_AND_EXIT)); assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-list.help"); } - - public void testUrlSpacesInPath() throws MalformedURLException { - CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(); - Path tmpDir = createTempDir().resolve("foo deps"); - String finalDir = tmpDir.toAbsolutePath().toUri().toURL().toString(); - CliTool.ExitStatus execute = new PluginCli(terminal).execute("install", finalDir); - assertThat(execute.status(), is(IO_ERROR.status())); - } } diff --git a/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java b/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java index 217d48a8565..c94b5e0b43a 100644 --- a/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java +++ b/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java @@ -139,14 +139,10 @@ public class StandaloneRunner extends CliTool { } public static byte[] copyToBytes(Path path) throws IOException { - try (InputStream is = Files.newInputStream(path)) { - if (is == null) { - throw new FileNotFoundException("Resource [" + path + "] not found in classpath"); - } - try (BytesStreamOutput out = new BytesStreamOutput()) { - copy(is, out); - return out.bytes().toBytes(); - } + try (InputStream is = Files.newInputStream(path); + BytesStreamOutput out = new BytesStreamOutput()) { + copy(is, out); + return out.bytes().toBytes(); } } @@ -177,7 +173,7 @@ public class StandaloneRunner extends CliTool { } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { StandaloneRunner pluginManager = new StandaloneRunner(); pluginManager.execute(args); } diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/BootstrapCliParserTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/BootstrapCliParserTests.java index 78085b201a3..92c9df1845d 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/BootstrapCliParserTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/BootstrapCliParserTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.cli.CliTool.ExitStatus; import org.elasticsearch.common.cli.CliToolTestCase; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.monitor.jvm.JvmInfo; import org.hamcrest.Matcher; @@ -167,7 +168,7 @@ public class BootstrapCliParserTests extends CliToolTestCase { assertThatTerminalOutput(containsString("Parameter [network.host] needs value")); } - public void testParsingErrors() { + public void testParsingErrors() throws Exception { BootstrapCLIParser parser = new BootstrapCLIParser(terminal); // unknown params @@ -229,12 +230,10 @@ public class BootstrapCliParserTests extends CliToolTestCase { public void testThatHelpfulErrorMessageIsGivenWhenParametersAreOutOfOrder() throws Exception { BootstrapCLIParser parser = new BootstrapCLIParser(terminal); - try { - parser.parse("start", new String[]{"--foo=bar", "-Dbaz=qux"}); - fail("expected IllegalArgumentException for out-of-order parameters"); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage(), containsString("must be before any parameters starting with --")); - } + UserError e = expectThrows(UserError.class, () -> { + parser.parse("start", new String[]{"--foo=bar", "-Dbaz=qux"}); + }); + assertThat(e.getMessage(), containsString("must be before any parameters starting with --")); } private void registerProperties(String ... systemProperties) { diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/common/cli/CliToolTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/common/cli/CliToolTests.java index 92ab945dfca..d5b494d9849 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/common/cli/CliToolTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/common/cli/CliToolTests.java @@ -71,9 +71,9 @@ public class CliToolTests extends CliToolTestCase { final AtomicReference executed = new AtomicReference<>(false); final NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override - public CliTool.ExitStatus execute(Settings settings, Environment env) { + public CliTool.ExitStatus execute(Settings settings, Environment env) throws UserError { executed.set(true); - return CliTool.ExitStatus.USAGE; + throw new UserError(CliTool.ExitStatus.USAGE, "bad usage"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); @@ -82,39 +82,7 @@ public class CliToolTests extends CliToolTestCase { assertCommandHasBeenExecuted(executed); } - public void testIOError() throws Exception { - Terminal terminal = new MockTerminal(); - final AtomicReference executed = new AtomicReference<>(false); - final NamedCommand cmd = new NamedCommand("cmd", terminal) { - @Override - public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { - executed.set(true); - throw new IOException("io error"); - } - }; - SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); - CliTool.ExitStatus status = tool.execute(); - assertStatus(status, CliTool.ExitStatus.IO_ERROR); - assertCommandHasBeenExecuted(executed); - } - - public void testCodeError() throws Exception { - Terminal terminal = new MockTerminal(); - final AtomicReference executed = new AtomicReference<>(false); - final NamedCommand cmd = new NamedCommand("cmd", terminal) { - @Override - public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { - executed.set(true); - throw new Exception("random error"); - } - }; - SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); - CliTool.ExitStatus status = tool.execute(); - assertStatus(status, CliTool.ExitStatus.CODE_ERROR); - assertCommandHasBeenExecuted(executed); - } - - public void testMultiCommand() { + public void testMultiCommand() throws Exception { Terminal terminal = new MockTerminal(); int count = randomIntBetween(2, 7); List> executed = new ArrayList<>(count); @@ -141,7 +109,7 @@ public class CliToolTests extends CliToolTestCase { } } - public void testMultiCommandUnknownCommand() { + public void testMultiCommandUnknownCommand() throws Exception { Terminal terminal = new MockTerminal(); int count = randomIntBetween(2, 7); List> executed = new ArrayList<>(count); @@ -184,7 +152,7 @@ public class CliToolTests extends CliToolTestCase { assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help"))); } - public void testMultiCommandToolHelp() { + public void testMultiCommandToolHelp() throws Exception { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand[] cmds = new NamedCommand[2]; cmds[0] = new NamedCommand("cmd0", terminal) { @@ -206,7 +174,7 @@ public class CliToolTests extends CliToolTestCase { assertThat(terminal.getTerminalOutput(), hasItem(containsString("tool help"))); } - public void testMultiCommandCmdHelp() { + public void testMultiCommandCmdHelp() throws Exception { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand[] cmds = new NamedCommand[2]; cmds[0] = new NamedCommand("cmd0", terminal) { @@ -228,31 +196,19 @@ public class CliToolTests extends CliToolTestCase { assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help"))); } - public void testThatThrowExceptionCanBeLogged() throws Exception { + public void testNonUserErrorPropagates() throws Exception { CaptureOutputTerminal terminal = new CaptureOutputTerminal(); NamedCommand cmd = new NamedCommand("cmd", terminal) { @Override public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { - throw new ElasticsearchException("error message"); + throw new IOException("error message"); } }; SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); - assertStatus(tool.execute(), CliTool.ExitStatus.CODE_ERROR); - assertThat(terminal.getTerminalOutput(), hasSize(1)); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message"))); - - // set env... and log stack trace - try { - System.setProperty(Terminal.DEBUG_SYSTEM_PROPERTY, "true"); - terminal = new CaptureOutputTerminal(); - assertStatus(new SingleCmdTool("tool", terminal, cmd).execute(), CliTool.ExitStatus.CODE_ERROR); - assertThat(terminal.getTerminalOutput(), hasSize(2)); - assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message"))); - // This class must be part of the stack strace - assertThat(terminal.getTerminalOutput(), hasItem(containsString(getClass().getName()))); - } finally { - System.clearProperty(Terminal.DEBUG_SYSTEM_PROPERTY); - } + IOException e = expectThrows(IOException.class, () -> { + tool.execute(); + }); + assertEquals("error message", e.getMessage()); } public void testMultipleLaunch() throws Exception { diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java index 2905f60598d..b61ba3c13ef 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java @@ -19,8 +19,8 @@ package org.elasticsearch.plugins; -import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; @@ -28,6 +28,7 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; @@ -43,6 +44,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.CliToolTestCase; import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; @@ -193,6 +195,16 @@ public class InstallPluginCommandTests extends ESTestCase { assertPlugin("fake", pluginDir, env); } + public void testSpaceInUrl() throws Exception { + Environment env = createEnv(); + Path pluginDir = createTempDir(); + String pluginZip = createPlugin("fake", pluginDir); + Path pluginZipWithSpaces = createTempFile("foo bar", ".zip"); + Files.copy(new URL(pluginZip).openStream(), pluginZipWithSpaces, StandardCopyOption.REPLACE_EXISTING); + installPlugin(pluginZipWithSpaces.toUri().toURL().toString(), env); + assertPlugin("fake", pluginDir, env); + } + public void testPluginsDirMissing() throws Exception { Environment env = createEnv(); Files.delete(env.pluginsFile()); @@ -211,7 +223,7 @@ public class InstallPluginCommandTests extends ESTestCase { IOException e = expectThrows(IOException.class, () -> { installPlugin(pluginZip, env); }); - assertTrue(e.getMessage(), e.getMessage().contains("Plugins directory is read only")); + assertTrue(e.getMessage(), e.getMessage().contains(env.pluginsFile().toString())); } assertInstallCleaned(env); } @@ -219,7 +231,7 @@ public class InstallPluginCommandTests extends ESTestCase { public void testBuiltinModule() throws Exception { Environment env = createEnv(); String pluginZip = createPlugin("lang-groovy", createTempDir()); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("is a system module")); @@ -288,7 +300,7 @@ public class InstallPluginCommandTests extends ESTestCase { Environment env = createEnv(); String pluginZip = createPlugin("fake", createTempDir()); installPlugin(pluginZip, env); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("already exists")); @@ -312,7 +324,7 @@ public class InstallPluginCommandTests extends ESTestCase { Path binDir = pluginDir.resolve("bin"); Files.createFile(binDir); String pluginZip = createPlugin("fake", pluginDir); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); @@ -326,7 +338,7 @@ public class InstallPluginCommandTests extends ESTestCase { Files.createDirectories(dirInBinDir); Files.createFile(dirInBinDir.resolve("somescript")); String pluginZip = createPlugin("fake", pluginDir); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin")); @@ -401,7 +413,7 @@ public class InstallPluginCommandTests extends ESTestCase { Path configDir = pluginDir.resolve("config"); Files.createFile(configDir); String pluginZip = createPlugin("fake", pluginDir); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); @@ -415,7 +427,7 @@ public class InstallPluginCommandTests extends ESTestCase { Files.createDirectories(dirInConfigDir); Files.createFile(dirInConfigDir.resolve("myconfig.yml")); String pluginZip = createPlugin("fake", pluginDir); - IOException e = expectThrows(IOException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { installPlugin(pluginZip, env); }); assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in config dir for plugin")); diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java index a643c835bd1..10fbc3c2696 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java @@ -28,6 +28,7 @@ import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.common.cli.CliTool; import org.elasticsearch.common.cli.CliToolTestCase; import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; @@ -66,7 +67,7 @@ public class RemovePluginCommandTests extends ESTestCase { public void testMissing() throws Exception { Environment env = createEnv(); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { removePlugin("dne", env); }); assertTrue(e.getMessage(), e.getMessage().contains("Plugin dne not found")); @@ -101,9 +102,10 @@ public class RemovePluginCommandTests extends ESTestCase { public void testBinNotDir() throws Exception { Environment env = createEnv(); Files.createDirectories(env.pluginsFile().resolve("elasticsearch")); - IllegalStateException e = expectThrows(IllegalStateException.class, () -> { + UserError e = expectThrows(UserError.class, () -> { removePlugin("elasticsearch", env); }); + assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); assertTrue(Files.exists(env.pluginsFile().resolve("elasticsearch"))); // did not remove assertTrue(Files.exists(env.binFile().resolve("elasticsearch"))); assertRemoveCleaned(env); From 07cba65c1b1e61d227d66f774116434edc180927 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Sat, 30 Jan 2016 20:18:58 -0500 Subject: [PATCH 14/49] Start to break lines at 140 characters Its what we say our maximum line length is in CONTRIBUTING.md and it'd be nice to have a check on that. Unfortunately, we don't actually wrap all lines at 140. --- .../transport/TransportService.java | 15 ++- .../transport/TransportSettings.java | 16 ++- .../transport/local/LocalTransport.java | 15 ++- .../local/LocalTransportChannel.java | 6 +- .../netty/MessageChannelHandler.java | 29 +++-- .../transport/netty/NettyTransport.java | 114 ++++++++++++------ .../netty/NettyTransportChannel.java | 6 +- .../netty/SizeHeaderFrameDecoder.java | 4 +- .../org/elasticsearch/tribe/TribeService.java | 52 +++++--- .../watcher/ResourceWatcherService.java | 3 +- 10 files changed, 171 insertions(+), 89 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/transport/TransportService.java b/core/src/main/java/org/elasticsearch/transport/TransportService.java index a6a1cab4f05..b050b2cb71f 100644 --- a/core/src/main/java/org/elasticsearch/transport/TransportService.java +++ b/core/src/main/java/org/elasticsearch/transport/TransportService.java @@ -31,6 +31,7 @@ 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.Scope; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; @@ -39,8 +40,8 @@ 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.tasks.TaskManager; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.TaskManager; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; @@ -56,6 +57,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Supplier; +import static java.util.Collections.emptyList; +import static org.elasticsearch.common.settings.Setting.listSetting; import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; /** @@ -92,9 +95,10 @@ public class TransportService extends AbstractLifecycleComponent> TRACE_LOG_INCLUDE_SETTING = Setting.listSetting("transport.tracer.include", Collections.emptyList(), Function.identity(), true, Setting.Scope.CLUSTER); - public static final Setting> TRACE_LOG_EXCLUDE_SETTING = Setting.listSetting("transport.tracer.exclude", Arrays.asList("internal:discovery/zen/fd*", TransportLivenessAction.NAME), Function.identity(), true, Setting.Scope.CLUSTER); - + public static final Setting> TRACE_LOG_INCLUDE_SETTING = listSetting("transport.tracer.include", emptyList(), + Function.identity(), true, Scope.CLUSTER); + public static final Setting> TRACE_LOG_EXCLUDE_SETTING = listSetting("transport.tracer.exclude", + Arrays.asList("internal:discovery/zen/fd*", TransportLivenessAction.NAME), Function.identity(), true, Scope.CLUSTER); private final ESLogger tracerLog; @@ -757,7 +761,8 @@ public class TransportService extends AbstractLifecycleComponent> HOST = Setting.listSetting("transport.host", emptyList(), s -> s, false, Setting.Scope.CLUSTER); - public static final Setting> PUBLISH_HOST = Setting.listSetting("transport.publish_host", HOST, s -> s, false, Setting.Scope.CLUSTER); - public static final Setting> BIND_HOST = Setting.listSetting("transport.bind_host", HOST, s -> s, false, Setting.Scope.CLUSTER); - public static final Setting PORT = new Setting<>("transport.tcp.port", "9300-9400", s -> s, false, Setting.Scope.CLUSTER); - public static final Setting PUBLISH_PORT = Setting.intSetting("transport.publish_port", -1, -1, false, Setting.Scope.CLUSTER); + public static final Setting> HOST = listSetting("transport.host", emptyList(), s -> s, false, Scope.CLUSTER); + public static final Setting> PUBLISH_HOST = listSetting("transport.publish_host", HOST, s -> s, false, Scope.CLUSTER); + public static final Setting> BIND_HOST = listSetting("transport.bind_host", HOST, s -> s, false, Scope.CLUSTER); + public static final Setting PORT = new Setting<>("transport.tcp.port", "9300-9400", s -> s, false, Scope.CLUSTER); + public static final Setting PUBLISH_PORT = intSetting("transport.publish_port", -1, -1, false, Scope.CLUSTER); public static final String DEFAULT_PROFILE = "default"; - public static final Setting TRANSPORT_PROFILES_SETTING = Setting.groupSetting("transport.profiles.", true, Setting.Scope.CLUSTER); + public static final Setting TRANSPORT_PROFILES_SETTING = groupSetting("transport.profiles.", true, Scope.CLUSTER); private TransportSettings() { diff --git a/core/src/main/java/org/elasticsearch/transport/local/LocalTransport.java b/core/src/main/java/org/elasticsearch/transport/local/LocalTransport.java index 7a41bf626c6..a5db72b9b5f 100644 --- a/core/src/main/java/org/elasticsearch/transport/local/LocalTransport.java +++ b/core/src/main/java/org/elasticsearch/transport/local/LocalTransport.java @@ -97,7 +97,8 @@ public class LocalTransport extends AbstractLifecycleComponent implem int queueSize = this.settings.getAsInt(TRANSPORT_LOCAL_QUEUE, -1); logger.debug("creating [{}] workers, queue_size [{}]", workerCount, queueSize); final ThreadFactory threadFactory = EsExecutors.daemonThreadFactory(this.settings, LOCAL_TRANSPORT_THREAD_NAME_PREFIX); - this.workers = EsExecutors.newFixed(LOCAL_TRANSPORT_THREAD_NAME_PREFIX, workerCount, queueSize, threadFactory, threadPool.getThreadContext()); + this.workers = EsExecutors.newFixed(LOCAL_TRANSPORT_THREAD_NAME_PREFIX, workerCount, queueSize, threadFactory, + threadPool.getThreadContext()); this.namedWriteableRegistry = namedWriteableRegistry; } @@ -199,7 +200,8 @@ public class LocalTransport extends AbstractLifecycleComponent implem } @Override - public void sendRequest(final DiscoveryNode node, final long requestId, final String action, final TransportRequest request, TransportRequestOptions options) throws IOException, TransportException { + public void sendRequest(final DiscoveryNode node, final long requestId, final String action, final TransportRequest request, + TransportRequestOptions options) throws IOException, TransportException { final Version version = Version.smallest(node.version(), this.version); try (BytesStreamOutput stream = new BytesStreamOutput()) { @@ -237,7 +239,8 @@ public class LocalTransport extends AbstractLifecycleComponent implem return this.workers; } - protected void messageReceived(byte[] data, String action, LocalTransport sourceTransport, Version version, @Nullable final Long sendRequestId) { + protected void messageReceived(byte[] data, String action, LocalTransport sourceTransport, Version version, + @Nullable final Long sendRequestId) { Transports.assertTransportThread(); try { transportServiceAdapter.received(data.length); @@ -278,7 +281,8 @@ public class LocalTransport extends AbstractLifecycleComponent implem stream = new NamedWriteableAwareStreamInput(stream, namedWriteableRegistry); final String action = stream.readString(); transportServiceAdapter.onRequestReceived(requestId, action); - final LocalTransportChannel transportChannel = new LocalTransportChannel(this, transportServiceAdapter, sourceTransport, action, requestId, version); + final LocalTransportChannel transportChannel = new LocalTransportChannel(this, transportServiceAdapter, sourceTransport, action, + requestId, version); try { final RequestHandlerRegistry reg = transportServiceAdapter.getRequestHandler(action); if (reg == null) { @@ -334,7 +338,8 @@ public class LocalTransport extends AbstractLifecycleComponent implem try { response.readFrom(buffer); } catch (Throwable e) { - handleException(handler, new TransportSerializationException("Failed to deserialize response of type [" + response.getClass().getName() + "]", e)); + handleException(handler, new TransportSerializationException( + "Failed to deserialize response of type [" + response.getClass().getName() + "]", e)); return; } handleParsedResponse(response, handler); diff --git a/core/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java b/core/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java index aad31fd8ccd..41eb7354098 100644 --- a/core/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java +++ b/core/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java @@ -46,7 +46,8 @@ public class LocalTransportChannel implements TransportChannel { private final long requestId; private final Version version; - public LocalTransportChannel(LocalTransport sourceTransport, TransportServiceAdapter sourceTransportServiceAdapter, LocalTransport targetTransport, String action, long requestId, Version version) { + public LocalTransportChannel(LocalTransport sourceTransport, TransportServiceAdapter sourceTransportServiceAdapter, + LocalTransport targetTransport, String action, long requestId, Version version) { this.sourceTransport = sourceTransport; this.sourceTransportServiceAdapter = sourceTransportServiceAdapter; this.targetTransport = targetTransport; @@ -94,7 +95,8 @@ public class LocalTransportChannel implements TransportChannel { public void sendResponse(Throwable error) throws IOException { BytesStreamOutput stream = new BytesStreamOutput(); writeResponseExceptionHeader(stream); - RemoteTransportException tx = new RemoteTransportException(targetTransport.nodeName(), targetTransport.boundAddress().boundAddresses()[0], action, error); + RemoteTransportException tx = new RemoteTransportException(targetTransport.nodeName(), + targetTransport.boundAddress().boundAddresses()[0], action, error); stream.writeThrowable(tx); final byte[] data = stream.bytes().toBytes(); diff --git a/core/src/main/java/org/elasticsearch/transport/netty/MessageChannelHandler.java b/core/src/main/java/org/elasticsearch/transport/netty/MessageChannelHandler.java index 6732b26ddbb..fca979f9bc9 100644 --- a/core/src/main/java/org/elasticsearch/transport/netty/MessageChannelHandler.java +++ b/core/src/main/java/org/elasticsearch/transport/netty/MessageChannelHandler.java @@ -116,7 +116,9 @@ public class MessageChannelHandler extends SimpleChannelUpstreamHandler { } catch (NotCompressedException ex) { int maxToRead = Math.min(buffer.readableBytes(), 10); int offset = buffer.readerIndex(); - StringBuilder sb = new StringBuilder("stream marked as compressed, but no compressor found, first [").append(maxToRead).append("] content bytes out of [").append(buffer.readableBytes()).append("] readable bytes with message size [").append(size).append("] ").append("] are ["); + StringBuilder sb = new StringBuilder("stream marked as compressed, but no compressor found, first [").append(maxToRead) + .append("] content bytes out of [").append(buffer.readableBytes()) + .append("] readable bytes with message size [").append(size).append("] ").append("] are ["); for (int i = 0; i < maxToRead; i++) { sb.append(buffer.getByte(offset + i)).append(","); } @@ -134,15 +136,17 @@ public class MessageChannelHandler extends SimpleChannelUpstreamHandler { final int nextByte = streamIn.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 + "], readerIndex [" + buffer.readerIndex() + "] vs expected [" + expectedIndexReader + "]; resetting"); + throw new IllegalStateException("Message not fully read (request) for requestId [" + requestId + "], action [" + action + + "], readerIndex [" + buffer.readerIndex() + "] vs expected [" + expectedIndexReader + "]; resetting"); } if (buffer.readerIndex() < expectedIndexReader) { - throw new IllegalStateException("Message is fully read (request), yet there are " + (expectedIndexReader - buffer.readerIndex()) + " remaining bytes; resetting"); + throw new IllegalStateException("Message is fully read (request), yet there are " + + (expectedIndexReader - buffer.readerIndex()) + " remaining bytes; resetting"); } if (buffer.readerIndex() > expectedIndexReader) { - throw new IllegalStateException("Message read past expected size (request) for requestId [" + requestId + "], action [" - + action + "], readerIndex [" + buffer.readerIndex() + "] vs expected [" + expectedIndexReader + "]; resetting"); + throw new IllegalStateException( + "Message read past expected size (request) for requestId [" + requestId + "], action [" + action + + "], readerIndex [" + buffer.readerIndex() + "] vs expected [" + expectedIndexReader + "]; resetting"); } } else { @@ -163,11 +167,12 @@ public class MessageChannelHandler extends SimpleChannelUpstreamHandler { + handler + "], error [" + TransportStatus.isError(status) + "]; resetting"); } if (buffer.readerIndex() < expectedIndexReader) { - throw new IllegalStateException("Message is fully read (response), yet there are " + (expectedIndexReader - buffer.readerIndex()) + " remaining bytes; resetting"); + throw new IllegalStateException("Message is fully read (response), yet there are " + + (expectedIndexReader - buffer.readerIndex()) + " remaining bytes; resetting"); } if (buffer.readerIndex() > expectedIndexReader) { - throw new IllegalStateException("Message read past expected size (response) for requestId [" + requestId + "], handler [" - + handler + "], error [" + TransportStatus.isError(status) + "]; resetting"); + throw new IllegalStateException("Message read past expected size (response) for requestId [" + requestId + + "], handler [" + handler + "], error [" + TransportStatus.isError(status) + "]; resetting"); } } @@ -193,7 +198,8 @@ public class MessageChannelHandler extends SimpleChannelUpstreamHandler { try { response.readFrom(buffer); } catch (Throwable e) { - handleException(handler, new TransportSerializationException("Failed to deserialize response of type [" + response.getClass().getName() + "]", e)); + handleException(handler, new TransportSerializationException( + "Failed to deserialize response of type [" + response.getClass().getName() + "]", e)); return; } try { @@ -247,7 +253,8 @@ public class MessageChannelHandler extends SimpleChannelUpstreamHandler { buffer = new NamedWriteableAwareStreamInput(buffer, transport.namedWriteableRegistry); final String action = buffer.readString(); transportServiceAdapter.onRequestReceived(requestId, action); - final NettyTransportChannel transportChannel = new NettyTransportChannel(transport, transportServiceAdapter, action, channel, requestId, version, profileName); + final NettyTransportChannel transportChannel = new NettyTransportChannel(transport, transportServiceAdapter, action, channel, + requestId, version, profileName); try { final RequestHandlerRegistry reg = transportServiceAdapter.getRequestHandler(action); if (reg == null) { diff --git a/core/src/main/java/org/elasticsearch/transport/netty/NettyTransport.java b/core/src/main/java/org/elasticsearch/transport/netty/NettyTransport.java index 99fbac17b69..8b174ecb19c 100644 --- a/core/src/main/java/org/elasticsearch/transport/netty/NettyTransport.java +++ b/core/src/main/java/org/elasticsearch/transport/netty/NettyTransport.java @@ -42,6 +42,7 @@ import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.network.NetworkService.TcpSettings; import org.elasticsearch.common.network.NetworkUtils; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Scope; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.InetSocketTransportAddress; @@ -119,6 +120,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.settings.Setting.boolSetting; +import static org.elasticsearch.common.settings.Setting.byteSizeSetting; +import static org.elasticsearch.common.settings.Setting.intSetting; +import static org.elasticsearch.common.settings.Setting.timeSetting; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.transport.NetworkExceptionHelper.isCloseConnectionException; import static org.elasticsearch.common.transport.NetworkExceptionHelper.isConnectException; @@ -143,21 +148,33 @@ public class NettyTransport extends AbstractLifecycleComponent implem public static final String TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX = "transport_client_boss"; public static final Setting WORKER_COUNT = new Setting<>("transport.netty.worker_count", - (s) -> Integer.toString(EsExecutors.boundedNumberOfProcessors(s) * 2), (s) -> Setting.parseInt(s, 1, "transport.netty.worker_count"), - false, Setting.Scope.CLUSTER); - public static final Setting CONNECTIONS_PER_NODE_RECOVERY = Setting.intSetting("transport.connections_per_node.recovery", 2, 1, false, Setting.Scope.CLUSTER); - public static final Setting CONNECTIONS_PER_NODE_BULK = Setting.intSetting("transport.connections_per_node.bulk", 3, 1, false, Setting.Scope.CLUSTER); - public static final Setting CONNECTIONS_PER_NODE_REG = Setting.intSetting("transport.connections_per_node.reg", 6, 1, false, Setting.Scope.CLUSTER); - public static final Setting CONNECTIONS_PER_NODE_STATE = Setting.intSetting("transport.connections_per_node.state", 1, 1, false, Setting.Scope.CLUSTER); - public static final Setting CONNECTIONS_PER_NODE_PING = Setting.intSetting("transport.connections_per_node.ping", 1, 1, false, Setting.Scope.CLUSTER); + (s) -> Integer.toString(EsExecutors.boundedNumberOfProcessors(s) * 2), + (s) -> Setting.parseInt(s, 1, "transport.netty.worker_count"), false, Setting.Scope.CLUSTER); + public static final Setting CONNECTIONS_PER_NODE_RECOVERY = intSetting("transport.connections_per_node.recovery", 2, 1, false, + Scope.CLUSTER); + public static final Setting CONNECTIONS_PER_NODE_BULK = intSetting("transport.connections_per_node.bulk", 3, 1, false, + Scope.CLUSTER); + public static final Setting CONNECTIONS_PER_NODE_REG = intSetting("transport.connections_per_node.reg", 6, 1, false, + Scope.CLUSTER); + public static final Setting CONNECTIONS_PER_NODE_STATE = intSetting("transport.connections_per_node.state", 1, 1, false, + Scope.CLUSTER); + public static final Setting CONNECTIONS_PER_NODE_PING = intSetting("transport.connections_per_node.ping", 1, 1, false, + Scope.CLUSTER); // the scheduled internal ping interval setting, defaults to disabled (-1) - public static final Setting PING_SCHEDULE = Setting.timeSetting("transport.ping_schedule", TimeValue.timeValueSeconds(-1), false, Setting.Scope.CLUSTER); - public static final Setting TCP_BLOCKING_CLIENT = Setting.boolSetting("transport.tcp.blocking_client", TcpSettings.TCP_BLOCKING_CLIENT, false, Setting.Scope.CLUSTER); - public static final Setting TCP_CONNECT_TIMEOUT = Setting.timeSetting("transport.tcp.connect_timeout", TcpSettings.TCP_CONNECT_TIMEOUT, false, Setting.Scope.CLUSTER); - public static final Setting TCP_NO_DELAY = Setting.boolSetting("transport.tcp_no_delay", TcpSettings.TCP_NO_DELAY, false, Setting.Scope.CLUSTER); - public static final Setting TCP_KEEP_ALIVE = Setting.boolSetting("transport.tcp.keep_alive", TcpSettings.TCP_KEEP_ALIVE, false, Setting.Scope.CLUSTER); - public static final Setting TCP_BLOCKING_SERVER = Setting.boolSetting("transport.tcp.blocking_server", TcpSettings.TCP_BLOCKING_SERVER, false, Setting.Scope.CLUSTER); - public static final Setting TCP_REUSE_ADDRESS = Setting.boolSetting("transport.tcp.reuse_address", TcpSettings.TCP_REUSE_ADDRESS, false, Setting.Scope.CLUSTER); + public static final Setting PING_SCHEDULE = timeSetting("transport.ping_schedule", TimeValue.timeValueSeconds(-1), false, + Setting.Scope.CLUSTER); + public static final Setting TCP_BLOCKING_CLIENT = boolSetting("transport.tcp.blocking_client", TcpSettings.TCP_BLOCKING_CLIENT, + false, Setting.Scope.CLUSTER); + public static final Setting TCP_CONNECT_TIMEOUT = timeSetting("transport.tcp.connect_timeout", + TcpSettings.TCP_CONNECT_TIMEOUT, false, Setting.Scope.CLUSTER); + public static final Setting TCP_NO_DELAY = boolSetting("transport.tcp_no_delay", TcpSettings.TCP_NO_DELAY, false, + Setting.Scope.CLUSTER); + public static final Setting TCP_KEEP_ALIVE = boolSetting("transport.tcp.keep_alive", TcpSettings.TCP_KEEP_ALIVE, false, + Setting.Scope.CLUSTER); + public static final Setting TCP_BLOCKING_SERVER = boolSetting("transport.tcp.blocking_server", TcpSettings.TCP_BLOCKING_SERVER, + false, Setting.Scope.CLUSTER); + public static final Setting TCP_REUSE_ADDRESS = boolSetting("transport.tcp.reuse_address", TcpSettings.TCP_REUSE_ADDRESS, + false, Setting.Scope.CLUSTER); public static final Setting TCP_SEND_BUFFER_SIZE = Setting.byteSizeSetting("transport.tcp.send_buffer_size", TcpSettings.TCP_SEND_BUFFER_SIZE, false, Setting.Scope.CLUSTER); public static final Setting TCP_RECEIVE_BUFFER_SIZE = Setting.byteSizeSetting("transport.tcp.receive_buffer_size", TcpSettings.TCP_RECEIVE_BUFFER_SIZE, false, Setting.Scope.CLUSTER); @@ -165,9 +182,9 @@ public class NettyTransport extends AbstractLifecycleComponent implem public static final Setting NETTY_MAX_CUMULATION_BUFFER_CAPACITY = Setting.byteSizeSetting("transport.netty.max_cumulation_buffer_capacity", new ByteSizeValue(-1), false, Setting.Scope.CLUSTER); public static final Setting NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS = Setting.intSetting("transport.netty.max_composite_buffer_components", -1, -1, false, Setting.Scope.CLUSTER); - // See AdaptiveReceiveBufferSizePredictor#DEFAULT_XXX for default values in netty..., we can use higher ones for us, even fixed one - public static final Setting NETTY_RECEIVE_PREDICTOR_SIZE = Setting.byteSizeSetting("transport.netty.receive_predictor_size", + public static final Setting NETTY_RECEIVE_PREDICTOR_SIZE = Setting.byteSizeSetting( + "transport.netty.receive_predictor_size", settings -> { long defaultReceiverPredictor = 512 * 1024; if (JvmInfo.jvmInfo().getMem().getDirectMemoryMax().bytes() > 0) { @@ -177,10 +194,11 @@ public class NettyTransport extends AbstractLifecycleComponent implem } return new ByteSizeValue(defaultReceiverPredictor).toString(); }, false, Setting.Scope.CLUSTER); - public static final Setting NETTY_RECEIVE_PREDICTOR_MIN = Setting.byteSizeSetting("transport.netty.receive_predictor_min", NETTY_RECEIVE_PREDICTOR_SIZE, false, Setting.Scope.CLUSTER); - public static final Setting NETTY_RECEIVE_PREDICTOR_MAX = Setting.byteSizeSetting("transport.netty.receive_predictor_max", NETTY_RECEIVE_PREDICTOR_SIZE, false, Setting.Scope.CLUSTER); - public static final Setting NETTY_BOSS_COUNT = Setting.intSetting("transport.netty.boss_count", 1, 1, false, Setting.Scope.CLUSTER); - + public static final Setting NETTY_RECEIVE_PREDICTOR_MIN = byteSizeSetting("transport.netty.receive_predictor_min", + NETTY_RECEIVE_PREDICTOR_SIZE, false, Scope.CLUSTER); + public static final Setting NETTY_RECEIVE_PREDICTOR_MAX = byteSizeSetting("transport.netty.receive_predictor_max", + NETTY_RECEIVE_PREDICTOR_SIZE, false, Scope.CLUSTER); + public static final Setting NETTY_BOSS_COUNT = intSetting("transport.netty.boss_count", 1, 1, false, Scope.CLUSTER); protected final NetworkService networkService; protected final Version version; @@ -226,7 +244,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem final ScheduledPing scheduledPing; @Inject - public NettyTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version, NamedWriteableRegistry namedWriteableRegistry) { + public NettyTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version, + NamedWriteableRegistry namedWriteableRegistry) { super(settings); this.threadPool = threadPool; this.networkService = networkService; @@ -252,7 +271,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem if (receivePredictorMax.bytes() == receivePredictorMin.bytes()) { receiveBufferSizePredictorFactory = new FixedReceiveBufferSizePredictorFactory((int) receivePredictorMax.bytes()); } else { - receiveBufferSizePredictorFactory = new AdaptiveReceiveBufferSizePredictorFactory((int) receivePredictorMin.bytes(), (int) receivePredictorMin.bytes(), (int) receivePredictorMax.bytes()); + receiveBufferSizePredictorFactory = new AdaptiveReceiveBufferSizePredictorFactory((int) receivePredictorMin.bytes(), + (int) receivePredictorMin.bytes(), (int) receivePredictorMax.bytes()); } this.scheduledPing = new ScheduledPing(); @@ -305,7 +325,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem String name = entry.getKey(); if (!Strings.hasLength(name)) { - logger.info("transport profile configured without a name. skipping profile with settings [{}]", profileSettings.toDelimitedString(',')); + logger.info("transport profile configured without a name. skipping profile with settings [{}]", + profileSettings.toDelimitedString(',')); continue; } else if (TransportSettings.DEFAULT_PROFILE.equals(name)) { profileSettings = settingsBuilder() @@ -345,13 +366,16 @@ public class NettyTransport extends AbstractLifecycleComponent implem private ClientBootstrap createClientBootstrap() { if (blockingClient) { - clientBootstrap = new ClientBootstrap(new OioClientSocketChannelFactory(Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX)))); + clientBootstrap = new ClientBootstrap(new OioClientSocketChannelFactory( + Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX)))); } else { int bossCount = NETTY_BOSS_COUNT.get(settings); - clientBootstrap = new ClientBootstrap(new NioClientSocketChannelFactory( - Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX)), - bossCount, - new NioWorkerPool(Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX)), workerCount), + clientBootstrap = new ClientBootstrap( + new NioClientSocketChannelFactory( + Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX)), + bossCount, + new NioWorkerPool(Executors.newCachedThreadPool( + daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX)), workerCount), new HashedWheelTimer(daemonThreadFactory(settings, "transport_client_timer")))); } clientBootstrap.setPipelineFactory(configureClientChannelPipelineFactory()); @@ -403,12 +427,14 @@ public class NettyTransport extends AbstractLifecycleComponent implem boolean fallbackReuseAddress = settings.getAsBoolean("transport.netty.reuse_address", TcpSettings.TCP_REUSE_ADDRESS.get(settings)); fallbackSettingsBuilder.put("reuse_address", fallbackReuseAddress); - ByteSizeValue fallbackTcpSendBufferSize = settings.getAsBytesSize("transport.netty.tcp_send_buffer_size", TcpSettings.TCP_SEND_BUFFER_SIZE.get(settings)); + ByteSizeValue fallbackTcpSendBufferSize = settings.getAsBytesSize("transport.netty.tcp_send_buffer_size", + TCP_SEND_BUFFER_SIZE.get(settings)); if (fallbackTcpSendBufferSize.bytes() >= 0) { fallbackSettingsBuilder.put("tcp_send_buffer_size", fallbackTcpSendBufferSize); } - ByteSizeValue fallbackTcpBufferSize = settings.getAsBytesSize("transport.netty.tcp_receive_buffer_size", TcpSettings.TCP_RECEIVE_BUFFER_SIZE.get(settings)); + ByteSizeValue fallbackTcpBufferSize = settings.getAsBytesSize("transport.netty.tcp_receive_buffer_size", + TCP_RECEIVE_BUFFER_SIZE.get(settings)); if (fallbackTcpBufferSize.bytes() >= 0) { fallbackSettingsBuilder.put("tcp_receive_buffer_size", fallbackTcpBufferSize); } @@ -485,7 +511,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem return boundSocket.get(); } - private BoundTransportAddress createBoundTransportAddress(String name, Settings profileSettings, List boundAddresses) { + private BoundTransportAddress createBoundTransportAddress(String name, Settings profileSettings, + List boundAddresses) { String[] boundAddressesHostStrings = new String[boundAddresses.size()]; TransportAddress[] transportBoundAddresses = new TransportAddress[boundAddresses.size()]; for (int i = 0; i < boundAddresses.size(); i++) { @@ -531,7 +558,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem // TODO: In case of DEFAULT_PROFILE we should probably fail here, as publish address does not match any bound address // In case of a custom profile, we might use the publish address of the default profile publishPort = boundAddresses.get(0).getPort(); - logger.warn("Publish port not found by matching publish address [{}] to bound addresses [{}], falling back to port [{}] of first bound address", publishInetAddress, boundAddresses, publishPort); + logger.warn("Publish port not found by matching publish address [{}] to bound addresses [{}], " + + "falling back to port [{}] of first bound address", publishInetAddress, boundAddresses, publishPort); } final TransportAddress publishAddress = new InetSocketTransportAddress(new InetSocketAddress(publishInetAddress, publishPort)); @@ -549,8 +577,13 @@ public class NettyTransport extends AbstractLifecycleComponent implem ByteSizeValue tcpSendBufferSize = TCP_SEND_BUFFER_SIZE.getDefault(settings); ByteSizeValue tcpReceiveBufferSize = TCP_RECEIVE_BUFFER_SIZE.getDefault(settings); - logger.debug("using profile[{}], worker_count[{}], port[{}], bind_host[{}], publish_host[{}], compress[{}], connect_timeout[{}], connections_per_node[{}/{}/{}/{}/{}], receive_predictor[{}->{}]", - name, workerCount, port, bindHost, publishHost, compress, connectTimeout, connectionsPerNodeRecovery, connectionsPerNodeBulk, connectionsPerNodeReg, connectionsPerNodeState, connectionsPerNodePing, receivePredictorMin, receivePredictorMax); + if (logger.isDebugEnabled()) { + logger.debug("using profile[{}], worker_count[{}], port[{}], bind_host[{}], publish_host[{}], compress[{}], " + + "connect_timeout[{}], connections_per_node[{}/{}/{}/{}/{}], receive_predictor[{}->{}]", + name, workerCount, port, bindHost, publishHost, compress, connectTimeout, connectionsPerNodeRecovery, + connectionsPerNodeBulk, connectionsPerNodeReg, connectionsPerNodeState, connectionsPerNodePing, receivePredictorMin, + receivePredictorMax); + } final ThreadFactory bossFactory = daemonThreadFactory(this.settings, HTTP_SERVER_BOSS_THREAD_NAME_PREFIX, name); final ThreadFactory workerFactory = daemonThreadFactory(this.settings, HTTP_SERVER_WORKER_THREAD_NAME_PREFIX, name); @@ -739,7 +772,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem return; } if (isCloseConnectionException(e.getCause())) { - logger.trace("close connection exception caught on transport layer [{}], disconnecting from relevant node", e.getCause(), ctx.getChannel()); + logger.trace("close connection exception caught on transport layer [{}], disconnecting from relevant node", e.getCause(), + ctx.getChannel()); // close the channel, which will cause a node to be disconnected if relevant ctx.getChannel().close(); disconnectFromNodeChannel(ctx.getChannel(), e.getCause()); @@ -754,7 +788,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem ctx.getChannel().close(); disconnectFromNodeChannel(ctx.getChannel(), e.getCause()); } else if (e.getCause() instanceof CancelledKeyException) { - logger.trace("cancelled key exception caught on transport layer [{}], disconnecting from relevant node", e.getCause(), ctx.getChannel()); + logger.trace("cancelled key exception caught on transport layer [{}], disconnecting from relevant node", e.getCause(), + ctx.getChannel()); // close the channel as safe measure, which will cause a node to be disconnected if relevant ctx.getChannel().close(); disconnectFromNodeChannel(ctx.getChannel(), e.getCause()); @@ -800,7 +835,8 @@ public class NettyTransport extends AbstractLifecycleComponent implem } @Override - public void sendRequest(final DiscoveryNode node, final long requestId, final String action, final TransportRequest request, TransportRequestOptions options) throws IOException, TransportException { + public void sendRequest(final DiscoveryNode node, final long requestId, final String action, final TransportRequest request, + TransportRequestOptions options) throws IOException, TransportException { Channel targetChannel = nodeChannel(node, options); @@ -902,7 +938,9 @@ public class NettyTransport extends AbstractLifecycleComponent implem if (light) { nodeChannels = connectToChannelsLight(node); } else { - nodeChannels = new NodeChannels(new Channel[connectionsPerNodeRecovery], new Channel[connectionsPerNodeBulk], new Channel[connectionsPerNodeReg], new Channel[connectionsPerNodeState], new Channel[connectionsPerNodePing]); + nodeChannels = new NodeChannels(new Channel[connectionsPerNodeRecovery], new Channel[connectionsPerNodeBulk], + new Channel[connectionsPerNodeReg], new Channel[connectionsPerNodeState], + new Channel[connectionsPerNodePing]); try { connectToChannels(nodeChannels, node); } catch (Throwable e) { diff --git a/core/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java b/core/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java index aaf33c2fd5a..c89523074dc 100644 --- a/core/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java +++ b/core/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java @@ -53,7 +53,8 @@ public class NettyTransportChannel implements TransportChannel { private final long requestId; private final String profileName; - public NettyTransportChannel(NettyTransport transport, TransportServiceAdapter transportServiceAdapter, String action, Channel channel, long requestId, Version version, String profileName) { + public NettyTransportChannel(NettyTransport transport, TransportServiceAdapter transportServiceAdapter, String action, Channel channel, + long requestId, Version version, String profileName) { this.transportServiceAdapter = transportServiceAdapter; this.version = version; this.transport = transport; @@ -119,7 +120,8 @@ public class NettyTransportChannel implements TransportChannel { public void sendResponse(Throwable error) throws IOException { BytesStreamOutput stream = new BytesStreamOutput(); stream.skip(NettyHeader.HEADER_SIZE); - RemoteTransportException tx = new RemoteTransportException(transport.nodeName(), transport.wrapAddress(channel.getLocalAddress()), action, error); + RemoteTransportException tx = new RemoteTransportException(transport.nodeName(), transport.wrapAddress(channel.getLocalAddress()), + action, error); stream.writeThrowable(tx); byte status = 0; status = TransportStatus.setResponse(status); diff --git a/core/src/main/java/org/elasticsearch/transport/netty/SizeHeaderFrameDecoder.java b/core/src/main/java/org/elasticsearch/transport/netty/SizeHeaderFrameDecoder.java index f38dc1dc02d..aab83d293d8 100644 --- a/core/src/main/java/org/elasticsearch/transport/netty/SizeHeaderFrameDecoder.java +++ b/core/src/main/java/org/elasticsearch/transport/netty/SizeHeaderFrameDecoder.java @@ -80,8 +80,8 @@ public class SizeHeaderFrameDecoder extends FrameDecoder { } // safety against too large frames being sent if (dataLen > NINETY_PER_HEAP_SIZE) { - throw new TooLongFrameException( - "transport content length received [" + new ByteSizeValue(dataLen) + "] exceeded [" + new ByteSizeValue(NINETY_PER_HEAP_SIZE) + "]"); + throw new TooLongFrameException("transport content length received [" + new ByteSizeValue(dataLen) + "] exceeded [" + + new ByteSizeValue(NINETY_PER_HEAP_SIZE) + "]"); } if (buffer.readableBytes() < dataLen + 6) { diff --git a/core/src/main/java/org/elasticsearch/tribe/TribeService.java b/core/src/main/java/org/elasticsearch/tribe/TribeService.java index 44d35305a60..88c4dc75222 100644 --- a/core/src/main/java/org/elasticsearch/tribe/TribeService.java +++ b/core/src/main/java/org/elasticsearch/tribe/TribeService.java @@ -20,6 +20,7 @@ package org.elasticsearch.tribe; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; + import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.master.TransportMasterNodeReadAction; import org.elasticsearch.cluster.ClusterChangedEvent; @@ -83,8 +84,10 @@ import static java.util.Collections.unmodifiableMap; */ public class TribeService extends AbstractLifecycleComponent { - public static final ClusterBlock TRIBE_METADATA_BLOCK = new ClusterBlock(10, "tribe node, metadata not allowed", false, false, RestStatus.BAD_REQUEST, EnumSet.of(ClusterBlockLevel.METADATA_READ, ClusterBlockLevel.METADATA_WRITE)); - public static final ClusterBlock TRIBE_WRITE_BLOCK = new ClusterBlock(11, "tribe node, write not allowed", false, false, RestStatus.BAD_REQUEST, EnumSet.of(ClusterBlockLevel.WRITE)); + public static final ClusterBlock TRIBE_METADATA_BLOCK = new ClusterBlock(10, "tribe node, metadata not allowed", false, false, + RestStatus.BAD_REQUEST, EnumSet.of(ClusterBlockLevel.METADATA_READ, ClusterBlockLevel.METADATA_WRITE)); + public static final ClusterBlock TRIBE_WRITE_BLOCK = new ClusterBlock(11, "tribe node, write not allowed", false, false, + RestStatus.BAD_REQUEST, EnumSet.of(ClusterBlockLevel.WRITE)); public static Settings processSettings(Settings settings) { if (TRIBE_NAME_SETTING.exists(settings)) { @@ -106,7 +109,8 @@ public class TribeService extends AbstractLifecycleComponent { Settings.Builder sb = Settings.builder().put(settings); sb.put(Node.NODE_CLIENT_SETTING.getKey(), true); // this node should just act as a node client sb.put(DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey(), "local"); // a tribe node should not use zen discovery - sb.put(DiscoveryService.INITIAL_STATE_TIMEOUT_SETTING.getKey(), 0); // nothing is going to be discovered, since no master will be elected + // nothing is going to be discovered, since no master will be elected + sb.put(DiscoveryService.INITIAL_STATE_TIMEOUT_SETTING.getKey(), 0); if (sb.get("cluster.name") == null) { sb.put("cluster.name", "tribe_" + Strings.randomBase64UUID()); // make sure it won't join other tribe nodes in the same JVM } @@ -114,7 +118,8 @@ public class TribeService extends AbstractLifecycleComponent { return sb.build(); } - private static final Setting TRIBE_NAME_SETTING = Setting.simpleString("tribe.name", false, Setting.Scope.CLUSTER); // internal settings only + // internal settings only + private static final Setting TRIBE_NAME_SETTING = Setting.simpleString("tribe.name", false, Setting.Scope.CLUSTER); private final ClusterService clusterService; private final String[] blockIndicesWrite; private final String[] blockIndicesRead; @@ -125,14 +130,20 @@ public class TribeService extends AbstractLifecycleComponent { if (ON_CONFLICT_ANY.equals(s) || ON_CONFLICT_DROP.equals(s) || s.startsWith(ON_CONFLICT_PREFER)) { return s; } - throw new IllegalArgumentException("Invalid value for [tribe.on_conflict] must be either [any, drop or start with prefer_] but was: " +s); + throw new IllegalArgumentException( + "Invalid value for [tribe.on_conflict] must be either [any, drop or start with prefer_] but was: " + s); }, false, Setting.Scope.CLUSTER); - public static final Setting BLOCKS_METADATA_SETTING = Setting.boolSetting("tribe.blocks.metadata", false, false, Setting.Scope.CLUSTER); - public static final Setting BLOCKS_WRITE_SETTING = Setting.boolSetting("tribe.blocks.write", false, false, Setting.Scope.CLUSTER); - public static final Setting> BLOCKS_WRITE_INDICES_SETTING = Setting.listSetting("tribe.blocks.write.indices", Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); - public static final Setting> BLOCKS_READ_INDICES_SETTING = Setting.listSetting("tribe.blocks.read.indices", Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); - public static final Setting> BLOCKS_METADATA_INDICES_SETTING = Setting.listSetting("tribe.blocks.metadata.indices", Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); + public static final Setting BLOCKS_METADATA_SETTING = Setting.boolSetting("tribe.blocks.metadata", false, false, + Setting.Scope.CLUSTER); + public static final Setting BLOCKS_WRITE_SETTING = Setting.boolSetting("tribe.blocks.write", false, false, + Setting.Scope.CLUSTER); + public static final Setting> BLOCKS_WRITE_INDICES_SETTING = Setting.listSetting("tribe.blocks.write.indices", + Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); + public static final Setting> BLOCKS_READ_INDICES_SETTING = Setting.listSetting("tribe.blocks.read.indices", + Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); + public static final Setting> BLOCKS_METADATA_INDICES_SETTING = Setting.listSetting("tribe.blocks.metadata.indices", + Collections.emptyList(), Function.identity(), false, Setting.Scope.CLUSTER); private final String onConflict; private final Set droppedIndices = ConcurrentCollections.newConcurrentSet(); @@ -304,7 +315,8 @@ public class TribeService extends AbstractLifecycleComponent { tribeAttr.put(attr.key, attr.value); } tribeAttr.put(TRIBE_NAME_SETTING.getKey(), tribeName); - DiscoveryNode discoNode = new DiscoveryNode(tribe.name(), tribe.id(), tribe.getHostName(), tribe.getHostAddress(), tribe.address(), unmodifiableMap(tribeAttr), tribe.version()); + DiscoveryNode discoNode = new DiscoveryNode(tribe.name(), tribe.id(), tribe.getHostName(), tribe.getHostAddress(), + tribe.address(), unmodifiableMap(tribeAttr), tribe.version()); clusterStateChanged = true; logger.info("[{}] adding node [{}]", tribeName, discoNode); nodes.put(discoNode); @@ -328,7 +340,8 @@ public class TribeService extends AbstractLifecycleComponent { // always make sure to update the metadata and routing table, in case // there are changes in them (new mapping, shards moving from initializing to started) routingTable.add(tribeState.routingTable().index(index.getIndex())); - Settings tribeSettings = Settings.builder().put(tribeIndex.getSettings()).put(TRIBE_NAME_SETTING.getKey(), tribeName).build(); + Settings tribeSettings = Settings.builder().put(tribeIndex.getSettings()) + .put(TRIBE_NAME_SETTING.getKey(), tribeName).build(); metaData.put(IndexMetaData.builder(tribeIndex).settings(tribeSettings)); } } @@ -357,7 +370,8 @@ public class TribeService extends AbstractLifecycleComponent { } else if (ON_CONFLICT_DROP.equals(onConflict)) { // drop the indices, there is a conflict clusterStateChanged = true; - logger.info("[{}] dropping index {} due to conflict with [{}]", tribeName, tribeIndex.getIndex(), existingFromTribe); + logger.info("[{}] dropping index {} due to conflict with [{}]", tribeName, tribeIndex.getIndex(), + existingFromTribe); removeIndex(blocks, metaData, routingTable, tribeIndex); droppedIndices.add(tribeIndex.getIndex().getName()); } else if (onConflict.startsWith(ON_CONFLICT_PREFER)) { @@ -366,7 +380,8 @@ public class TribeService extends AbstractLifecycleComponent { if (tribeName.equals(preferredTribeName)) { // the new one is hte preferred one, replace... clusterStateChanged = true; - logger.info("[{}] adding index {}, preferred over [{}]", tribeName, tribeIndex.getIndex(), existingFromTribe); + logger.info("[{}] adding index {}, preferred over [{}]", tribeName, tribeIndex.getIndex(), + existingFromTribe); removeIndex(blocks, metaData, routingTable, tribeIndex); addNewIndex(tribeState, blocks, metaData, routingTable, tribeIndex); } // else: either the existing one is the preferred one, or we haven't seen one, carry on @@ -378,17 +393,20 @@ public class TribeService extends AbstractLifecycleComponent { if (!clusterStateChanged) { return currentState; } else { - return ClusterState.builder(currentState).incrementVersion().blocks(blocks).nodes(nodes).metaData(metaData).routingTable(routingTable.build()).build(); + return ClusterState.builder(currentState).incrementVersion().blocks(blocks).nodes(nodes).metaData(metaData) + .routingTable(routingTable.build()).build(); } } - private void removeIndex(ClusterBlocks.Builder blocks, MetaData.Builder metaData, RoutingTable.Builder routingTable, IndexMetaData index) { + private void removeIndex(ClusterBlocks.Builder blocks, MetaData.Builder metaData, RoutingTable.Builder routingTable, + IndexMetaData index) { metaData.remove(index.getIndex().getName()); routingTable.remove(index.getIndex().getName()); blocks.removeIndexBlocks(index.getIndex().getName()); } - private void addNewIndex(ClusterState tribeState, ClusterBlocks.Builder blocks, MetaData.Builder metaData, RoutingTable.Builder routingTable, IndexMetaData tribeIndex) { + private void addNewIndex(ClusterState tribeState, ClusterBlocks.Builder blocks, MetaData.Builder metaData, + RoutingTable.Builder routingTable, IndexMetaData tribeIndex) { Settings tribeSettings = Settings.builder().put(tribeIndex.getSettings()).put(TRIBE_NAME_SETTING.getKey(), tribeName).build(); metaData.put(IndexMetaData.builder(tribeIndex).settings(tribeSettings)); routingTable.add(tribeState.routingTable().index(tribeIndex.getIndex())); diff --git a/core/src/main/java/org/elasticsearch/watcher/ResourceWatcherService.java b/core/src/main/java/org/elasticsearch/watcher/ResourceWatcherService.java index 5ff6525a428..7c1cd060952 100644 --- a/core/src/main/java/org/elasticsearch/watcher/ResourceWatcherService.java +++ b/core/src/main/java/org/elasticsearch/watcher/ResourceWatcherService.java @@ -83,7 +83,8 @@ public class ResourceWatcherService extends AbstractLifecycleComponent Date: Tue, 12 Jan 2016 12:04:42 +0100 Subject: [PATCH 15/49] Add operation counter for IndexShard Adds a container that represents a resource with reference counting capabilities. Provides operations to suspend acquisition of new references. Useful for resource management when resources are intermittently unavailable. Closes #15956 --- .../concurrent/SuspendableRefContainer.java | 117 ++++++++++++++++++ .../SuspendableRefContainerTests.java | 115 +++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 core/src/main/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainer.java create mode 100644 core/src/test/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainerTests.java diff --git a/core/src/main/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainer.java b/core/src/main/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainer.java new file mode 100644 index 00000000000..2afb78591dd --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainer.java @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.util.concurrent; + +import org.elasticsearch.common.lease.Releasable; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Container that represents a resource with reference counting capabilities. Provides operations to suspend acquisition of new references. + * This is useful for resource management when resources are intermittently unavailable. + * + * Assumes less than Integer.MAX_VALUE references are concurrently being held at one point in time. + */ +public final class SuspendableRefContainer { + private static final int TOTAL_PERMITS = Integer.MAX_VALUE; + private final Semaphore semaphore; + + public SuspendableRefContainer() { + // fair semaphore to ensure that blockAcquisition() does not starve under thread contention + this.semaphore = new Semaphore(TOTAL_PERMITS, true); + } + + /** + * Tries acquiring a reference. Returns reference holder if reference acquisition is not blocked at the time of invocation (see + * {@link #blockAcquisition()}). Returns null if reference acquisition is blocked at the time of invocation. + * + * @return reference holder if reference acquisition is not blocked, null otherwise + * @throws InterruptedException if the current thread is interrupted + */ + public Releasable tryAcquire() throws InterruptedException { + if (semaphore.tryAcquire(1, 0, TimeUnit.SECONDS)) { // the untimed tryAcquire methods do not honor the fairness setting + return idempotentRelease(1); + } else { + return null; + } + } + + /** + * Acquires a reference. Blocks if reference acquisition is blocked at the time of invocation. + * + * @return reference holder + * @throws InterruptedException if the current thread is interrupted + */ + public Releasable acquire() throws InterruptedException { + semaphore.acquire(); + return idempotentRelease(1); + } + + /** + * Acquires a reference. Blocks if reference acquisition is blocked at the time of invocation. + * + * @return reference holder + */ + public Releasable acquireUninterruptibly() { + semaphore.acquireUninterruptibly(); + return idempotentRelease(1); + } + + /** + * Disables reference acquisition and waits until all existing references are released. + * When released, reference acquisition is enabled again. + * This guarantees that between successful acquisition and release, no one is holding a reference. + * + * @return references holder to all references + */ + public Releasable blockAcquisition() { + semaphore.acquireUninterruptibly(TOTAL_PERMITS); + return idempotentRelease(TOTAL_PERMITS); + } + + /** + * Helper method that ensures permits are only released once + * + * @return reference holder + */ + private Releasable idempotentRelease(int permits) { + AtomicBoolean closed = new AtomicBoolean(); + return () -> { + if (closed.compareAndSet(false, true)) { + semaphore.release(permits); + } + }; + } + + /** + * Returns the number of references currently being held. + */ + public int activeRefs() { + int availablePermits = semaphore.availablePermits(); + if (availablePermits == 0) { + // when blockAcquisition is holding all permits + return 0; + } else { + return TOTAL_PERMITS - availablePermits; + } + } +} diff --git a/core/src/test/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainerTests.java b/core/src/test/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainerTests.java new file mode 100644 index 00000000000..83db2d4a7c6 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/common/util/concurrent/SuspendableRefContainerTests.java @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.util.concurrent; + +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class SuspendableRefContainerTests extends ESTestCase { + + public void testBasicAcquire() throws InterruptedException { + SuspendableRefContainer refContainer = new SuspendableRefContainer(); + assertThat(refContainer.activeRefs(), equalTo(0)); + + Releasable lock1 = randomLockingMethod(refContainer); + assertThat(refContainer.activeRefs(), equalTo(1)); + Releasable lock2 = randomLockingMethod(refContainer); + assertThat(refContainer.activeRefs(), equalTo(2)); + lock1.close(); + assertThat(refContainer.activeRefs(), equalTo(1)); + lock1.close(); // check idempotence + assertThat(refContainer.activeRefs(), equalTo(1)); + lock2.close(); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + + public void testAcquisitionBlockingBlocksNewAcquisitions() throws InterruptedException { + SuspendableRefContainer refContainer = new SuspendableRefContainer(); + assertThat(refContainer.activeRefs(), equalTo(0)); + + try (Releasable block = refContainer.blockAcquisition()) { + assertThat(refContainer.activeRefs(), equalTo(0)); + assertThat(refContainer.tryAcquire(), nullValue()); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + try (Releasable lock = refContainer.tryAcquire()) { + assertThat(refContainer.activeRefs(), equalTo(1)); + } + + // same with blocking acquire + AtomicBoolean acquired = new AtomicBoolean(); + Thread t = new Thread(() -> { + try (Releasable lock = randomBoolean() ? refContainer.acquire() : refContainer.acquireUninterruptibly()) { + acquired.set(true); + assertThat(refContainer.activeRefs(), equalTo(1)); + } catch (InterruptedException e) { + fail("Interrupted"); + } + }); + try (Releasable block = refContainer.blockAcquisition()) { + assertThat(refContainer.activeRefs(), equalTo(0)); + t.start(); + // check that blocking acquire really blocks + assertThat(acquired.get(), equalTo(false)); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + t.join(); + assertThat(acquired.get(), equalTo(true)); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + + public void testAcquisitionBlockingWaitsOnExistingAcquisitions() throws InterruptedException { + SuspendableRefContainer refContainer = new SuspendableRefContainer(); + + AtomicBoolean acquired = new AtomicBoolean(); + Thread t = new Thread(() -> { + try (Releasable block = refContainer.blockAcquisition()) { + acquired.set(true); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + }); + try (Releasable lock = randomLockingMethod(refContainer)) { + assertThat(refContainer.activeRefs(), equalTo(1)); + t.start(); + assertThat(acquired.get(), equalTo(false)); + assertThat(refContainer.activeRefs(), equalTo(1)); + } + t.join(); + assertThat(acquired.get(), equalTo(true)); + assertThat(refContainer.activeRefs(), equalTo(0)); + } + + private Releasable randomLockingMethod(SuspendableRefContainer refContainer) throws InterruptedException { + switch (randomInt(2)) { + case 0: return refContainer.tryAcquire(); + case 1: return refContainer.acquire(); + case 2: return refContainer.acquireUninterruptibly(); + } + throw new IllegalArgumentException("randomLockingMethod inconsistent"); + } +} From 10b5ffcda5400acabb74c397d100b0034f5fe33a Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Mon, 1 Feb 2016 11:11:57 +0100 Subject: [PATCH 16/49] Add proper handoff between old and new copy of relocating primary shard When primary relocation completes, a cluster state is propagated that deactivates the old primary and marks the new primary as active. As cluster state changes are not applied synchronously on all nodes, there can be a time interval where the relocation target has processed the cluster state and believes to be the active primary and the relocation source has not yet processed the cluster state update and still believes itself to be the active primary. This commit ensures that, before completing the relocation, the reloction source deactivates writing to its store and delegates requests to the relocation target. Closes #15900 --- .../flush/TransportShardFlushAction.java | 2 +- .../refresh/TransportShardRefreshAction.java | 2 +- .../action/index/TransportIndexAction.java | 4 +- .../TransportReplicationAction.java | 177 +++++++++++------- .../action/index/MappingUpdatedAction.java | 6 +- .../elasticsearch/index/shard/IndexShard.java | 97 +++++----- .../shard/IndexShardRelocatedException.java | 8 +- .../cluster/IndicesClusterStateService.java | 8 +- .../indices/flush/SyncedFlushService.java | 2 +- .../indices/recovery/RecoverySource.java | 10 +- .../recovery/RecoverySourceHandler.java | 14 +- .../indices/recovery/RecoveryState.java | 2 +- .../indices/recovery/RecoveryTarget.java | 4 +- .../SharedFSRecoverySourceHandler.java | 4 - .../recovery/StartRecoveryRequest.java | 11 +- .../TransportChannelResponseHandler.java | 16 +- .../ClusterStateCreationUtils.java | 28 +-- .../TransportReplicationActionTests.java | 137 +++++++++++--- .../action/shard/ShardStateActionTests.java | 13 +- .../index/shard/IndexShardTests.java | 137 ++++++++++++-- .../indices/IndicesLifecycleListenerIT.java | 3 +- .../flush/SyncedFlushSingleNodeTests.java | 6 +- .../recovery/IndexPrimaryRelocationIT.java | 89 +++++++++ .../indices/recovery/IndexRecoveryIT.java | 14 +- .../recovery/RecoverySourceHandlerTests.java | 6 +- .../recovery/StartRecoveryRequestTests.java | 5 +- .../recovery/FullRollingRestartIT.java | 4 +- .../test/InternalTestCluster.java | 2 +- 28 files changed, 576 insertions(+), 235 deletions(-) create mode 100644 core/src/test/java/org/elasticsearch/indices/recovery/IndexPrimaryRelocationIT.java diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java b/core/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java index 8f7fce89c09..302bdafc471 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/flush/TransportShardFlushAction.java @@ -58,7 +58,7 @@ public class TransportShardFlushAction extends TransportReplicationAction shardOperationOnPrimary(MetaData metaData, ShardFlushRequest shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, ShardFlushRequest shardRequest) { IndexShard indexShard = indicesService.indexServiceSafe(shardRequest.shardId().getIndex()).getShard(shardRequest.shardId().id()); indexShard.flush(shardRequest.getRequest()); logger.trace("{} flush request executed on primary", indexShard.shardId()); diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java b/core/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java index 7c9979e7374..2dd41f7801d 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/refresh/TransportShardRefreshAction.java @@ -60,7 +60,7 @@ public class TransportShardRefreshAction extends TransportReplicationAction shardOperationOnPrimary(MetaData metaData, BasicReplicationRequest shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, BasicReplicationRequest shardRequest) { IndexShard indexShard = indicesService.indexServiceSafe(shardRequest.shardId().getIndex()).getShard(shardRequest.shardId().id()); indexShard.refresh("api"); logger.trace("{} refresh request executed on primary", indexShard.shardId()); diff --git a/core/src/main/java/org/elasticsearch/action/index/TransportIndexAction.java b/core/src/main/java/org/elasticsearch/action/index/TransportIndexAction.java index 33bf3547d0b..fdd018c51f2 100644 --- a/core/src/main/java/org/elasticsearch/action/index/TransportIndexAction.java +++ b/core/src/main/java/org/elasticsearch/action/index/TransportIndexAction.java @@ -140,7 +140,7 @@ public class TransportIndexAction extends TransportReplicationAction shardOperationOnPrimary(MetaData metaData, IndexRequest request) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, IndexRequest request) throws Exception { // validate, if routing is required, that we got routing IndexMetaData indexMetaData = metaData.index(request.shardId().getIndex()); @@ -200,7 +200,7 @@ public class TransportIndexAction extends TransportReplicationAction executeIndexRequestOnPrimary(IndexRequest request, IndexShard indexShard, MappingUpdatedAction mappingUpdatedAction) throws Throwable { + public static WriteResult executeIndexRequestOnPrimary(IndexRequest request, IndexShard indexShard, MappingUpdatedAction mappingUpdatedAction) throws Exception { Engine.Index operation = prepareIndexOperationOnPrimary(request, indexShard); Mapping update = operation.parsedDoc().dynamicMappingsUpdate(); final ShardId shardId = indexShard.shardId(); diff --git a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index c40d3fb579a..07e4322f6b0 100644 --- a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -56,6 +56,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardState; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesService; @@ -156,10 +157,11 @@ public abstract class TransportReplicationAction shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable; + protected abstract Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception; /** * Replica operation on nodes with replica copies @@ -299,7 +301,7 @@ public abstract class TransportReplicationAction handler = TransportChannelResponseHandler.emptyResponseHandler(logger, channel, extraMessage); transportService.sendRequest(clusterService.localNode(), transportReplicaAction, request, handler); } @@ -352,6 +354,7 @@ public abstract class TransportReplicationAction * Note that as soon as we move to replication action, state responsibility is transferred to {@link ReplicationPhase}. */ - final class PrimaryPhase extends AbstractRunnable { + class PrimaryPhase extends AbstractRunnable { private final Request request; + private final ShardId shardId; private final TransportChannel channel; private final ClusterState state; private final AtomicBoolean finished = new AtomicBoolean(); - private Releasable indexShardReference; + private IndexShardReference indexShardReference; PrimaryPhase(Request request, TransportChannel channel) { this.state = clusterService.state(); this.request = request; + assert request.shardId() != null : "request shardId must be set prior to primary phase"; + this.shardId = request.shardId(); this.channel = channel; } @Override public void onFailure(Throwable e) { + if (ExceptionsHelper.status(e) == RestStatus.CONFLICT) { + if (logger.isTraceEnabled()) { + logger.trace("failed to execute [{}] on [{}]", e, request, shardId); + } + } else { + if (logger.isDebugEnabled()) { + logger.debug("failed to execute [{}] on [{}]", e, request, shardId); + } + } finishAsFailed(e); } @Override protected void doRun() throws Exception { // request shardID was set in ReroutePhase - assert request.shardId() != null : "request shardID must be set prior to primary phase"; - final ShardId shardId = request.shardId(); final String writeConsistencyFailure = checkWriteConsistency(shardId); if (writeConsistencyFailure != null) { finishBecauseUnavailable(shardId, writeConsistencyFailure); return; } - final ReplicationPhase replicationPhase; - try { - indexShardReference = getIndexShardOperationsCounter(shardId); + // closed in finishAsFailed(e) in the case of error + indexShardReference = getIndexShardReferenceOnPrimary(shardId); + if (indexShardReference.isRelocated() == false) { + // execute locally Tuple primaryResponse = shardOperationOnPrimary(state.metaData(), request); if (logger.isTraceEnabled()) { logger.trace("action [{}] completed on shard [{}] for request [{}] with cluster state version [{}]", transportPrimaryAction, shardId, request, state.version()); } - replicationPhase = new ReplicationPhase(primaryResponse.v2(), primaryResponse.v1(), shardId, channel, indexShardReference); - } catch (Throwable e) { - if (ExceptionsHelper.status(e) == RestStatus.CONFLICT) { - if (logger.isTraceEnabled()) { - logger.trace("failed to execute [{}] on [{}]", e, request, shardId); - } - } else { - if (logger.isDebugEnabled()) { - logger.debug("failed to execute [{}] on [{}]", e, request, shardId); - } - } - finishAsFailed(e); - return; + ReplicationPhase replicationPhase = new ReplicationPhase(primaryResponse.v2(), primaryResponse.v1(), shardId, channel, indexShardReference); + finishAndMoveToReplication(replicationPhase); + } else { + // delegate primary phase to relocation target + // it is safe to execute primary phase on relocation target as there are no more in-flight operations where primary + // phase is executed on local shard and all subsequent operations are executed on relocation target as primary phase. + final ShardRouting primary = indexShardReference.routingEntry(); + indexShardReference.close(); + assert primary.relocating() : "indexShard is marked as relocated but routing isn't" + primary; + DiscoveryNode relocatingNode = state.nodes().get(primary.relocatingNodeId()); + transportService.sendRequest(relocatingNode, transportPrimaryAction, request, transportOptions, + TransportChannelResponseHandler.responseHandler(logger, TransportReplicationAction.this::newResponseInstance, channel, + "rerouting indexing to target primary " + primary)); } - finishAndMoveToReplication(replicationPhase); } /** @@ -721,10 +736,24 @@ public abstract class TransportReplicationAction readAllowedStates = EnumSet.of(IndexShardState.STARTED, IndexShardState.RELOCATED, IndexShardState.POST_RECOVERY); + private static final EnumSet readAllowedStates = EnumSet.of(IndexShardState.STARTED, IndexShardState.RELOCATED, IndexShardState.POST_RECOVERY); + // for primaries, we only allow to write when actually started (so the cluster has decided we started) + // in case we have a relocation of a primary, we also allow to write after phase 2 completed, where the shard may be + // in state RECOVERING or POST_RECOVERY. After a primary has been marked as RELOCATED, we only allow writes to the relocation target + // which can be either in POST_RECOVERY or already STARTED (this prevents writing concurrently to two primaries). + public static final EnumSet writeAllowedStatesForPrimary = EnumSet.of(IndexShardState.RECOVERING, IndexShardState.POST_RECOVERY, IndexShardState.STARTED); + // replication is also allowed while recovering, since we index also during recovery to replicas and rely on version checks to make sure its consistent + // a relocated shard can also be target of a replication if the relocation target has not been marked as active yet and is syncing it's changes back to the relocation source + private static final EnumSet writeAllowedStatesForReplica = EnumSet.of(IndexShardState.RECOVERING, IndexShardState.POST_RECOVERY, IndexShardState.STARTED, IndexShardState.RELOCATED); private final IndexSearcherWrapper searcherWrapper; @@ -250,7 +258,7 @@ public class IndexShard extends AbstractIndexShardComponent { } this.engineConfig = newEngineConfig(translogConfig, cachingPolicy); - this.indexShardOperationCounter = new IndexShardOperationCounter(logger, shardId); + this.suspendableRefContainer = new SuspendableRefContainer(); this.provider = provider; this.searcherWrapper = indexSearcherWrapper; this.percolatorQueriesRegistry = new PercolatorQueriesRegistry(shardId, indexSettings, newQueryShardContext()); @@ -321,6 +329,8 @@ public class IndexShard extends AbstractIndexShardComponent { * Updates the shards routing entry. This mutate the shards internal state depending * on the changes that get introduced by the new routing value. This method will persist shard level metadata * unless explicitly disabled. + * + * @throws IndexShardRelocatedException if shard is marked as relocated and relocation aborted */ public void updateRoutingEntry(final ShardRouting newRouting, final boolean persistState) { final ShardRouting currentRouting = this.shardRouting; @@ -368,6 +378,14 @@ public class IndexShard extends AbstractIndexShardComponent { } } } + + if (state == IndexShardState.RELOCATED && + (newRouting.relocating() == false || newRouting.equalsIgnoringMetaData(currentRouting) == false)) { + // if the shard is marked as RELOCATED we have to fail when any changes in shard routing occur (e.g. due to recovery + // failure / cancellation). The reason is that at the moment we cannot safely move back to STARTED without risking two + // active primaries. + throw new IndexShardRelocatedException(shardId(), "Shard is marked as relocated, cannot safely move to state " + newRouting.state()); + } this.shardRouting = newRouting; indexEventListener.shardRoutingChanged(this, currentRouting, newRouting); } finally { @@ -404,12 +422,16 @@ public class IndexShard extends AbstractIndexShardComponent { } public IndexShard relocated(String reason) throws IndexShardNotStartedException { - synchronized (mutex) { - if (state != IndexShardState.STARTED) { - throw new IndexShardNotStartedException(shardId, state); + try (Releasable block = suspendableRefContainer.blockAcquisition()) { + // no shard operation locks are being held here, move state from started to relocated + synchronized (mutex) { + if (state != IndexShardState.STARTED) { + throw new IndexShardNotStartedException(shardId, state); + } + changeState(IndexShardState.RELOCATED, reason); } - changeState(IndexShardState.RELOCATED, reason); } + return this; } @@ -796,7 +818,6 @@ public class IndexShard extends AbstractIndexShardComponent { refreshScheduledFuture = null; } changeState(IndexShardState.CLOSED, reason); - indexShardOperationCounter.decRef(); } finally { final Engine engine = this.currentEngineReference.getAndSet(null); try { @@ -810,7 +831,6 @@ public class IndexShard extends AbstractIndexShardComponent { } } - public IndexShard postRecovery(String reason) throws IndexShardStartedException, IndexShardRelocatedException, IndexShardClosedException { if (mapperService.hasMapping(PercolatorService.TYPE_NAME)) { refresh("percolator_load_queries"); @@ -967,16 +987,17 @@ public class IndexShard extends AbstractIndexShardComponent { IndexShardState state = this.state; // one time volatile read if (origin == Engine.Operation.Origin.PRIMARY) { - // for primaries, we only allow to write when actually started (so the cluster has decided we started) - // otherwise, we need to retry, we also want to still allow to index if we are relocated in case it fails - if (state != IndexShardState.STARTED && state != IndexShardState.RELOCATED) { - throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when started/recovering, origin [" + origin + "]"); + if (writeAllowedStatesForPrimary.contains(state) == false) { + throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when shard state is one of " + writeAllowedStatesForPrimary + ", origin [" + origin + "]"); + } + } else if (origin == Engine.Operation.Origin.RECOVERY) { + if (state != IndexShardState.RECOVERING) { + throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when recovering, origin [" + origin + "]"); } } else { - // for replicas, we allow to write also while recovering, since we index also during recovery to replicas - // and rely on version checks to make sure its consistent - if (state != IndexShardState.STARTED && state != IndexShardState.RELOCATED && state != IndexShardState.RECOVERING && state != IndexShardState.POST_RECOVERY) { - throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when started/recovering, origin [" + origin + "]"); + assert origin == Engine.Operation.Origin.REPLICA; + if (writeAllowedStatesForReplica.contains(state) == false) { + throw new IllegalIndexShardStateException(shardId, state, "operation only allowed when shard state is one of " + writeAllowedStatesForReplica + ", origin [" + origin + "]"); } } } @@ -995,7 +1016,7 @@ public class IndexShard extends AbstractIndexShardComponent { private void verifyNotClosed(Throwable suppressed) throws IllegalIndexShardStateException { IndexShardState state = this.state; // one time volatile read if (state == IndexShardState.CLOSED) { - final IllegalIndexShardStateException exc = new IllegalIndexShardStateException(shardId, state, "operation only allowed when not closed"); + final IllegalIndexShardStateException exc = new IndexShardClosedException(shardId, "operation only allowed when not closed"); if (suppressed != null) { exc.addSuppressed(suppressed); } @@ -1390,37 +1411,21 @@ public class IndexShard extends AbstractIndexShardComponent { idxSettings.getSettings().getAsTime(IndexingMemoryController.SHARD_INACTIVE_TIME_SETTING, IndexingMemoryController.SHARD_DEFAULT_INACTIVE_TIME)); } - private static class IndexShardOperationCounter extends AbstractRefCounted { - final private ESLogger logger; - private final ShardId shardId; - - public IndexShardOperationCounter(ESLogger logger, ShardId shardId) { - super("index-shard-operations-counter"); - this.logger = logger; - this.shardId = shardId; - } - - @Override - protected void closeInternal() { - logger.debug("operations counter reached 0, will not accept any further writes"); - } - - @Override - protected void alreadyClosed() { - throw new IndexShardClosedException(shardId, "could not increment operation counter. shard is closed."); + public Releasable acquirePrimaryOperationLock() { + verifyNotClosed(); + if (shardRouting.primary() == false) { + throw new IllegalIndexShardStateException(shardId, state, "shard is not a primary"); } + return suspendableRefContainer.acquireUninterruptibly(); } - public void incrementOperationCounter() { - indexShardOperationCounter.incRef(); + public Releasable acquireReplicaOperationLock() { + verifyNotClosed(); + return suspendableRefContainer.acquireUninterruptibly(); } - public void decrementOperationCounter() { - indexShardOperationCounter.decRef(); - } - - public int getOperationsCount() { - return Math.max(0, indexShardOperationCounter.refCount() - 1); // refCount is incremented on creation and decremented on close + public int getActiveOperationsCount() { + return suspendableRefContainer.activeRefs(); // refCount is incremented on creation and decremented on close } /** diff --git a/core/src/main/java/org/elasticsearch/index/shard/IndexShardRelocatedException.java b/core/src/main/java/org/elasticsearch/index/shard/IndexShardRelocatedException.java index 2d3c48cd4c5..043ad892777 100644 --- a/core/src/main/java/org/elasticsearch/index/shard/IndexShardRelocatedException.java +++ b/core/src/main/java/org/elasticsearch/index/shard/IndexShardRelocatedException.java @@ -29,10 +29,14 @@ import java.io.IOException; public class IndexShardRelocatedException extends IllegalIndexShardStateException { public IndexShardRelocatedException(ShardId shardId) { - super(shardId, IndexShardState.RELOCATED, "Already relocated"); + this(shardId, "Already relocated"); + } + + public IndexShardRelocatedException(ShardId shardId, String reason) { + super(shardId, IndexShardState.RELOCATED, reason); } public IndexShardRelocatedException(StreamInput in) throws IOException{ super(in); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index 8c2f23f7081..7fc12eb8bab 100644 --- a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -492,7 +492,11 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent> ongoingRecoveries = new HashMap<>(); synchronized void add(IndexShard shard, RecoverySourceHandler handler) { diff --git a/core/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/core/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 8cbdfca0221..26c288cfbca 100644 --- a/core/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/core/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -393,9 +393,11 @@ public class RecoverySourceHandler { } }); - - if (request.markAsRelocated()) { - // TODO what happens if the recovery process fails afterwards, we need to mark this back to started + if (isPrimaryRelocation()) { + /** + * if the recovery process fails after setting the shard state to RELOCATED, both relocation source and + * target are failed (see {@link IndexShard#updateRoutingEntry}). + */ try { shard.relocated("to " + request.targetNode()); } catch (IllegalIndexShardStateException e) { @@ -406,7 +408,11 @@ public class RecoverySourceHandler { } stopWatch.stop(); logger.trace("[{}][{}] finalizing recovery to {}: took [{}]", - indexName, shardId, request.targetNode(), stopWatch.totalTime()); + indexName, shardId, request.targetNode(), stopWatch.totalTime()); + } + + protected boolean isPrimaryRelocation() { + return request.recoveryType() == RecoveryState.Type.PRIMARY_RELOCATION; } /** diff --git a/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryState.java b/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryState.java index 92bfc87218a..d1c41d4b932 100644 --- a/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryState.java +++ b/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryState.java @@ -101,7 +101,7 @@ public class RecoveryState implements ToXContent, Streamable { STORE((byte) 0), SNAPSHOT((byte) 1), REPLICA((byte) 2), - RELOCATION((byte) 3); + PRIMARY_RELOCATION((byte) 3); private static final Type[] TYPES = new Type[Type.values().length]; diff --git a/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java b/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java index 0912a22a0f5..727bd0b6441 100644 --- a/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java +++ b/core/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java @@ -138,7 +138,6 @@ public class RecoveryTarget extends AbstractComponent implements IndexEventListe // create a new recovery status, and process... final long recoveryId = onGoingRecoveries.startRecovery(indexShard, sourceNode, listener, recoverySettings.activityTimeout()); threadPool.generic().execute(new RecoveryRunner(recoveryId)); - } protected void retryRecovery(final RecoveryStatus recoveryStatus, final Throwable reason, TimeValue retryAfter, final StartRecoveryRequest currentRequest) { @@ -178,7 +177,7 @@ public class RecoveryTarget extends AbstractComponent implements IndexEventListe return; } final StartRecoveryRequest request = new StartRecoveryRequest(recoveryStatus.shardId(), recoveryStatus.sourceNode(), clusterService.localNode(), - false, metadataSnapshot, recoveryStatus.state().getType(), recoveryStatus.recoveryId()); + metadataSnapshot, recoveryStatus.state().getType(), recoveryStatus.recoveryId()); final AtomicReference responseHolder = new AtomicReference<>(); try { @@ -267,7 +266,6 @@ public class RecoveryTarget extends AbstractComponent implements IndexEventListe onGoingRecoveries.failRecovery(recoveryStatus.recoveryId(), new RecoveryFailedException(request, "source shard is closed", cause), false); return; } - onGoingRecoveries.failRecovery(recoveryStatus.recoveryId(), new RecoveryFailedException(request, e), true); } } diff --git a/core/src/main/java/org/elasticsearch/indices/recovery/SharedFSRecoverySourceHandler.java b/core/src/main/java/org/elasticsearch/indices/recovery/SharedFSRecoverySourceHandler.java index 16bd1d46553..8d75c474791 100644 --- a/core/src/main/java/org/elasticsearch/indices/recovery/SharedFSRecoverySourceHandler.java +++ b/core/src/main/java/org/elasticsearch/indices/recovery/SharedFSRecoverySourceHandler.java @@ -84,8 +84,4 @@ public class SharedFSRecoverySourceHandler extends RecoverySourceHandler { return 0; } - private boolean isPrimaryRelocation() { - return request.recoveryType() == RecoveryState.Type.RELOCATION && shard.routingEntry().primary(); - } - } diff --git a/core/src/main/java/org/elasticsearch/indices/recovery/StartRecoveryRequest.java b/core/src/main/java/org/elasticsearch/indices/recovery/StartRecoveryRequest.java index 3a62f4f6352..49dd70a73f7 100644 --- a/core/src/main/java/org/elasticsearch/indices/recovery/StartRecoveryRequest.java +++ b/core/src/main/java/org/elasticsearch/indices/recovery/StartRecoveryRequest.java @@ -41,8 +41,6 @@ public class StartRecoveryRequest extends TransportRequest { private DiscoveryNode targetNode; - private boolean markAsRelocated; - private Store.MetadataSnapshot metadataSnapshot; private RecoveryState.Type recoveryType; @@ -56,12 +54,11 @@ public class StartRecoveryRequest extends TransportRequest { * @param sourceNode The node to recover from * @param targetNode The node to recover to */ - public StartRecoveryRequest(ShardId shardId, DiscoveryNode sourceNode, DiscoveryNode targetNode, boolean markAsRelocated, Store.MetadataSnapshot metadataSnapshot, RecoveryState.Type recoveryType, long recoveryId) { + public StartRecoveryRequest(ShardId shardId, DiscoveryNode sourceNode, DiscoveryNode targetNode, Store.MetadataSnapshot metadataSnapshot, RecoveryState.Type recoveryType, long recoveryId) { this.recoveryId = recoveryId; this.shardId = shardId; this.sourceNode = sourceNode; this.targetNode = targetNode; - this.markAsRelocated = markAsRelocated; this.recoveryType = recoveryType; this.metadataSnapshot = metadataSnapshot; } @@ -82,10 +79,6 @@ public class StartRecoveryRequest extends TransportRequest { return targetNode; } - public boolean markAsRelocated() { - return markAsRelocated; - } - public RecoveryState.Type recoveryType() { return recoveryType; } @@ -101,7 +94,6 @@ public class StartRecoveryRequest extends TransportRequest { shardId = ShardId.readShardId(in); sourceNode = DiscoveryNode.readNode(in); targetNode = DiscoveryNode.readNode(in); - markAsRelocated = in.readBoolean(); metadataSnapshot = new Store.MetadataSnapshot(in); recoveryType = RecoveryState.Type.fromId(in.readByte()); @@ -114,7 +106,6 @@ public class StartRecoveryRequest extends TransportRequest { shardId.writeTo(out); sourceNode.writeTo(out); targetNode.writeTo(out); - out.writeBoolean(markAsRelocated); metadataSnapshot.writeTo(out); out.writeByte(recoveryType.id()); } diff --git a/core/src/main/java/org/elasticsearch/transport/TransportChannelResponseHandler.java b/core/src/main/java/org/elasticsearch/transport/TransportChannelResponseHandler.java index 8c042cd1937..69fc73e4af0 100644 --- a/core/src/main/java/org/elasticsearch/transport/TransportChannelResponseHandler.java +++ b/core/src/main/java/org/elasticsearch/transport/TransportChannelResponseHandler.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; +import java.util.function.Supplier; /** * Base class for delegating transport response to a transport channel @@ -30,7 +31,7 @@ import java.io.IOException; public abstract class TransportChannelResponseHandler implements TransportResponseHandler { /** - * Convenience method for delegating an empty response to the provided changed + * Convenience method for delegating an empty response to the provided transport channel */ public static TransportChannelResponseHandler emptyResponseHandler(ESLogger logger, TransportChannel channel, String extraInfoOnError) { return new TransportChannelResponseHandler(logger, channel, extraInfoOnError) { @@ -41,6 +42,19 @@ public abstract class TransportChannelResponseHandler TransportChannelResponseHandler responseHandler(ESLogger logger, Supplier responseSupplier, TransportChannel channel, String extraInfoOnError) { + return new TransportChannelResponseHandler(logger, channel, extraInfoOnError) { + @Override + public T newInstance() { + return responseSupplier.get(); + } + }; + } + + private final ESLogger logger; private final TransportChannel channel; private final String extraInfoOnError; diff --git a/core/src/test/java/org/elasticsearch/action/support/replication/ClusterStateCreationUtils.java b/core/src/test/java/org/elasticsearch/action/support/replication/ClusterStateCreationUtils.java index 49a5e072e1f..8e7b70a3b21 100644 --- a/core/src/test/java/org/elasticsearch/action/support/replication/ClusterStateCreationUtils.java +++ b/core/src/test/java/org/elasticsearch/action/support/replication/ClusterStateCreationUtils.java @@ -56,12 +56,12 @@ public class ClusterStateCreationUtils { /** * Creates cluster state with and index that has one shard and #(replicaStates) replicas * - * @param index name of the index - * @param primaryLocal if primary should coincide with the local node in the cluster state - * @param primaryState state of primary - * @param replicaStates states of the replicas. length of this array determines also the number of replicas + * @param index name of the index + * @param activePrimaryLocal if active primary should coincide with the local node in the cluster state + * @param primaryState state of primary + * @param replicaStates states of the replicas. length of this array determines also the number of replicas */ - public static ClusterState state(String index, boolean primaryLocal, ShardRoutingState primaryState, ShardRoutingState... replicaStates) { + public static ClusterState state(String index, boolean activePrimaryLocal, ShardRoutingState primaryState, ShardRoutingState... replicaStates) { final int numberOfReplicas = replicaStates.length; int numberOfNodes = numberOfReplicas + 1; @@ -97,7 +97,7 @@ public class ClusterStateCreationUtils { String relocatingNode = null; UnassignedInfo unassignedInfo = null; if (primaryState != ShardRoutingState.UNASSIGNED) { - if (primaryLocal) { + if (activePrimaryLocal) { primaryNode = newNode(0).id(); unassignedNodes.remove(primaryNode); } else { @@ -173,13 +173,13 @@ public class ClusterStateCreationUtils { * Creates cluster state with and index that has one shard and as many replicas as numberOfReplicas. * Primary will be STARTED in cluster state but replicas will be one of UNASSIGNED, INITIALIZING, STARTED or RELOCATING. * - * @param index name of the index - * @param primaryLocal if primary should coincide with the local node in the cluster state - * @param numberOfReplicas number of replicas + * @param index name of the index + * @param activePrimaryLocal if active primary should coincide with the local node in the cluster state + * @param numberOfReplicas number of replicas */ - public static ClusterState stateWithStartedPrimary(String index, boolean primaryLocal, int numberOfReplicas) { + public static ClusterState stateWithActivePrimary(String index, boolean activePrimaryLocal, int numberOfReplicas) { int assignedReplicas = randomIntBetween(0, numberOfReplicas); - return stateWithStartedPrimary(index, primaryLocal, assignedReplicas, numberOfReplicas - assignedReplicas); + return stateWithActivePrimary(index, activePrimaryLocal, assignedReplicas, numberOfReplicas - assignedReplicas); } /** @@ -188,11 +188,11 @@ public class ClusterStateCreationUtils { * some (assignedReplicas) will be one of INITIALIZING, STARTED or RELOCATING. * * @param index name of the index - * @param primaryLocal if primary should coincide with the local node in the cluster state + * @param activePrimaryLocal if active primary should coincide with the local node in the cluster state * @param assignedReplicas number of replicas that should have INITIALIZING, STARTED or RELOCATING state * @param unassignedReplicas number of replicas that should be unassigned */ - public static ClusterState stateWithStartedPrimary(String index, boolean primaryLocal, int assignedReplicas, int unassignedReplicas) { + public static ClusterState stateWithActivePrimary(String index, boolean activePrimaryLocal, int assignedReplicas, int unassignedReplicas) { ShardRoutingState[] replicaStates = new ShardRoutingState[assignedReplicas + unassignedReplicas]; // no point in randomizing - node assignment later on does it too. for (int i = 0; i < assignedReplicas; i++) { @@ -201,7 +201,7 @@ public class ClusterStateCreationUtils { for (int i = assignedReplicas; i < replicaStates.length; i++) { replicaStates[i] = ShardRoutingState.UNASSIGNED; } - return state(index, primaryLocal, randomFrom(ShardRoutingState.STARTED, ShardRoutingState.RELOCATING), replicaStates); + return state(index, activePrimaryLocal, randomFrom(ShardRoutingState.STARTED, ShardRoutingState.RELOCATING), replicaStates); } /** diff --git a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java index 402a454649b..4542be25485 100644 --- a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java +++ b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java @@ -37,6 +37,7 @@ import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.ShardIterator; import org.elasticsearch.cluster.routing.ShardRouting; @@ -75,9 +76,10 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.action.support.replication.ClusterStateCreationUtils.state; -import static org.elasticsearch.action.support.replication.ClusterStateCreationUtils.stateWithStartedPrimary; +import static org.elasticsearch.action.support.replication.ClusterStateCreationUtils.stateWithActivePrimary; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.empty; @@ -225,7 +227,7 @@ public class TransportReplicationActionTests extends ESTestCase { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); - clusterService.setState(stateWithStartedPrimary(index, randomBoolean(), 3)); + clusterService.setState(stateWithActivePrimary(index, randomBoolean(), 3)); logger.debug("using state: \n{}", clusterService.state().prettyPrint()); @@ -249,33 +251,73 @@ public class TransportReplicationActionTests extends ESTestCase { assertIndexShardUninitialized(); } - public void testPrimaryPhaseExecutesRequest() throws InterruptedException, ExecutionException { + public void testPrimaryPhaseExecutesOrDelegatesRequestToRelocationTarget() throws InterruptedException, ExecutionException { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); - clusterService.setState(state(index, true, ShardRoutingState.STARTED, ShardRoutingState.STARTED)); + ClusterState state = stateWithActivePrimary(index, true, randomInt(5)); + clusterService.setState(state); Request request = new Request(shardId).timeout("1ms"); PlainActionFuture listener = new PlainActionFuture<>(); - TransportReplicationAction.PrimaryPhase primaryPhase = action.new PrimaryPhase(request, createTransportChannel(listener)); + AtomicBoolean movedToReplication = new AtomicBoolean(); + TransportReplicationAction.PrimaryPhase primaryPhase = action.new PrimaryPhase(request, createTransportChannel(listener)) { + @Override + void finishAndMoveToReplication(TransportReplicationAction.ReplicationPhase replicationPhase) { + super.finishAndMoveToReplication(replicationPhase); + movedToReplication.set(true); + } + }; + ShardRouting primaryShard = state.getRoutingTable().shardRoutingTable(shardId).primaryShard(); + boolean executeOnPrimary = true; + if (primaryShard.relocating() && randomBoolean()) { // whether shard has been marked as relocated already (i.e. relocation completed) + isRelocated.set(true); + indexShardRouting.set(primaryShard); + executeOnPrimary = false; + } primaryPhase.run(); - assertThat("request was not processed on primary", request.processedOnPrimary.get(), equalTo(true)); - final String replicaNodeId = clusterService.state().getRoutingTable().shardRoutingTable(index, shardId.id()).replicaShards().get(0).currentNodeId(); - final List requests = transport.getCapturedRequestsByTargetNodeAndClear().get(replicaNodeId); - assertThat(requests, notNullValue()); - assertThat(requests.size(), equalTo(1)); - assertThat("replica request was not sent", requests.get(0).action, equalTo("testAction[r]")); + assertThat(request.processedOnPrimary.get(), equalTo(executeOnPrimary)); + assertThat(movedToReplication.get(), equalTo(executeOnPrimary)); + if (executeOnPrimary == false) { + final List requests = transport.capturedRequestsByTargetNode().get(primaryShard.relocatingNodeId()); + assertThat(requests, notNullValue()); + assertThat(requests.size(), equalTo(1)); + assertThat("primary request was not delegated to relocation target", requests.get(0).action, equalTo("testAction[p]")); + } + } + + public void testPrimaryPhaseExecutesDelegatedRequestOnRelocationTarget() throws InterruptedException, ExecutionException { + final String index = "test"; + final ShardId shardId = new ShardId(index, "_na_", 0); + ClusterState state = state(index, true, ShardRoutingState.RELOCATING); + String primaryTargetNodeId = state.getRoutingTable().shardRoutingTable(shardId).primaryShard().relocatingNodeId(); + // simulate execution of the primary phase on the relocation target node + state = ClusterState.builder(state).nodes(DiscoveryNodes.builder(state.nodes()).localNodeId(primaryTargetNodeId)).build(); + clusterService.setState(state); + Request request = new Request(shardId).timeout("1ms"); + PlainActionFuture listener = new PlainActionFuture<>(); + AtomicBoolean movedToReplication = new AtomicBoolean(); + TransportReplicationAction.PrimaryPhase primaryPhase = action.new PrimaryPhase(request, createTransportChannel(listener)) { + @Override + void finishAndMoveToReplication(TransportReplicationAction.ReplicationPhase replicationPhase) { + super.finishAndMoveToReplication(replicationPhase); + movedToReplication.set(true); + } + }; + primaryPhase.run(); + assertThat("request was not processed on primary relocation target", request.processedOnPrimary.get(), equalTo(true)); + assertThat(movedToReplication.get(), equalTo(true)); } public void testAddedReplicaAfterPrimaryOperation() { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); // start with no replicas - clusterService.setState(stateWithStartedPrimary(index, true, 0)); + clusterService.setState(stateWithActivePrimary(index, true, 0)); logger.debug("--> using initial state:\n{}", clusterService.state().prettyPrint()); final ClusterState stateWithAddedReplicas = state(index, true, ShardRoutingState.STARTED, randomBoolean() ? ShardRoutingState.INITIALIZING : ShardRoutingState.STARTED); final Action actionWithAddedReplicaAfterPrimaryOp = new Action(Settings.EMPTY, "testAction", transportService, clusterService, threadPool) { @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception { final Tuple operationOnPrimary = super.shardOperationOnPrimary(metaData, shardRequest); // add replicas after primary operation ((TestClusterService) clusterService).setState(stateWithAddedReplicas); @@ -302,13 +344,13 @@ public class TransportReplicationActionTests extends ESTestCase { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); // start with a replica - clusterService.setState(state(index, true, ShardRoutingState.STARTED, randomBoolean() ? ShardRoutingState.INITIALIZING : ShardRoutingState.STARTED)); + clusterService.setState(state(index, true, ShardRoutingState.STARTED, randomBoolean() ? ShardRoutingState.INITIALIZING : ShardRoutingState.STARTED)); logger.debug("--> using initial state:\n{}", clusterService.state().prettyPrint()); final ClusterState stateWithRelocatingReplica = state(index, true, ShardRoutingState.STARTED, ShardRoutingState.RELOCATING); final Action actionWithRelocatingReplicasAfterPrimaryOp = new Action(Settings.EMPTY, "testAction", transportService, clusterService, threadPool) { @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception { final Tuple operationOnPrimary = super.shardOperationOnPrimary(metaData, shardRequest); // set replica to relocating ((TestClusterService) clusterService).setState(stateWithRelocatingReplica); @@ -341,7 +383,7 @@ public class TransportReplicationActionTests extends ESTestCase { final Action actionWithDeletedIndexAfterPrimaryOp = new Action(Settings.EMPTY, "testAction", transportService, clusterService, threadPool) { @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception { final Tuple operationOnPrimary = super.shardOperationOnPrimary(metaData, shardRequest); // delete index after primary op ((TestClusterService) clusterService).setState(stateWithDeletedIndex); @@ -432,7 +474,13 @@ public class TransportReplicationActionTests extends ESTestCase { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + ClusterState state = stateWithActivePrimary(index, true, randomInt(5)); + ShardRouting primaryShard = state.getRoutingTable().shardRoutingTable(shardId).primaryShard(); + if (primaryShard.relocating() && randomBoolean()) { + // simulate execution of the replication phase on the relocation target node after relocation source was marked as relocated + state = ClusterState.builder(state).nodes(DiscoveryNodes.builder(state.nodes()).localNodeId(primaryShard.relocatingNodeId())).build(); + } + clusterService.setState(state); final IndexShardRoutingTable shardRoutingTable = clusterService.state().routingTable().index(index).shard(shardId.id()); int assignedReplicas = 0; @@ -455,12 +503,19 @@ public class TransportReplicationActionTests extends ESTestCase { final String index = "test"; final ShardId shardId = new ShardId(index, "_na_", 0); - ClusterState state = stateWithStartedPrimary(index, true, randomInt(5)); + ClusterState state = stateWithActivePrimary(index, true, randomInt(5)); MetaData.Builder metaData = MetaData.builder(state.metaData()); Settings.Builder settings = Settings.builder().put(metaData.get(index).getSettings()); settings.put(IndexMetaData.SETTING_SHADOW_REPLICAS, true); metaData.put(IndexMetaData.builder(metaData.get(index)).settings(settings)); - clusterService.setState(ClusterState.builder(state).metaData(metaData)); + state = ClusterState.builder(state).metaData(metaData).build(); + + ShardRouting primaryShard = state.getRoutingTable().shardRoutingTable(shardId).primaryShard(); + if (primaryShard.relocating() && randomBoolean()) { + // simulate execution of the primary phase on the relocation target node + state = ClusterState.builder(state).nodes(DiscoveryNodes.builder(state.nodes()).localNodeId(primaryShard.relocatingNodeId())).build(); + } + clusterService.setState(state); final IndexShardRoutingTable shardRoutingTable = clusterService.state().routingTable().index(index).shard(shardId.id()); int assignedReplicas = 0; @@ -507,8 +562,9 @@ public class TransportReplicationActionTests extends ESTestCase { assertEquals(request.shardId, replicationRequest.shardId); } + String localNodeId = clusterService.state().getNodes().localNodeId(); // no request was sent to the local node - assertThat(nodesSentTo.keySet(), not(hasItem(clusterService.state().getNodes().localNodeId()))); + assertThat(nodesSentTo.keySet(), not(hasItem(localNodeId))); // requests were sent to the correct shard copies for (ShardRouting shard : clusterService.state().getRoutingTable().shardRoutingTable(shardId)) { @@ -518,11 +574,11 @@ public class TransportReplicationActionTests extends ESTestCase { if (shard.unassigned()) { continue; } - if (shard.primary() == false) { - nodesSentTo.remove(shard.currentNodeId()); + if (localNodeId.equals(shard.currentNodeId()) == false) { + assertThat(nodesSentTo.remove(shard.currentNodeId()), notNullValue()); } - if (shard.relocating()) { - nodesSentTo.remove(shard.relocatingNodeId()); + if (shard.relocating() && localNodeId.equals(shard.relocatingNodeId()) == false) { // for relocating primaries, we replicate from target to source if source is marked as relocated + assertThat(nodesSentTo.remove(shard.relocatingNodeId()), notNullValue()); } } @@ -629,6 +685,7 @@ public class TransportReplicationActionTests extends ESTestCase { // shard operation should be ongoing, so the counter is at 2 // we have to wait here because increment happens in thread assertBusy(() -> assertIndexShardCounter(2)); + assertThat(transport.capturedRequests().length, equalTo(0)); ((ActionWithDelay) action).countDownLatch.countDown(); t.join(); @@ -726,12 +783,28 @@ public class TransportReplicationActionTests extends ESTestCase { private final AtomicInteger count = new AtomicInteger(0); + private final AtomicBoolean isRelocated = new AtomicBoolean(false); + + private final AtomicReference indexShardRouting = new AtomicReference<>(); + /* * Returns testIndexShardOperationsCounter or initializes it if it was already created in this test run. * */ - private synchronized Releasable getOrCreateIndexShardOperationsCounter() { + private synchronized TransportReplicationAction.IndexShardReference getOrCreateIndexShardOperationsCounter() { count.incrementAndGet(); - return new Releasable() { + return new TransportReplicationAction.IndexShardReference() { + @Override + public boolean isRelocated() { + return isRelocated.get(); + } + + @Override + public ShardRouting routingEntry() { + ShardRouting shardRouting = indexShardRouting.get(); + assert shardRouting != null; + return shardRouting; + } + @Override public void close() { count.decrementAndGet(); @@ -783,7 +856,7 @@ public class TransportReplicationActionTests extends ESTestCase { } @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception { boolean executedBefore = shardRequest.processedOnPrimary.getAndSet(true); assert executedBefore == false : "request has already been executed on the primary"; return new Tuple<>(new Response(), shardRequest); @@ -805,7 +878,11 @@ public class TransportReplicationActionTests extends ESTestCase { } @Override - protected Releasable getIndexShardOperationsCounter(ShardId shardId) { + protected IndexShardReference getIndexShardReferenceOnPrimary(ShardId shardId) { + return getOrCreateIndexShardOperationsCounter(); + } + + protected IndexShardReference getIndexShardReferenceOnReplica(ShardId shardId) { return getOrCreateIndexShardOperationsCounter(); } } @@ -832,7 +909,7 @@ public class TransportReplicationActionTests extends ESTestCase { } @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) { return throwException(shardRequest.shardId()); } @@ -870,7 +947,7 @@ public class TransportReplicationActionTests extends ESTestCase { } @Override - protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Throwable { + protected Tuple shardOperationOnPrimary(MetaData metaData, Request shardRequest) throws Exception { awaitLatch(); return new Tuple<>(new Response(), shardRequest); } diff --git a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java index 8a13e6e6ddd..b6e69b27a5a 100644 --- a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java +++ b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.cluster.action.shard; import org.apache.lucene.index.CorruptIndexException; +import org.elasticsearch.action.support.replication.ClusterStateCreationUtils; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateObserver; @@ -33,7 +34,6 @@ import org.elasticsearch.cluster.routing.ShardsIterator; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.Discovery; -import org.elasticsearch.index.shard.ShardNotFoundException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.cluster.TestClusterService; import org.elasticsearch.test.transport.CapturingTransport; @@ -55,7 +55,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.LongConsumer; -import static org.elasticsearch.action.support.replication.ClusterStateCreationUtils.stateWithStartedPrimary; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -127,7 +126,7 @@ public class ShardStateActionTests extends ESTestCase { public void testSuccess() throws InterruptedException { final String index = "test"; - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); @@ -169,7 +168,7 @@ public class ShardStateActionTests extends ESTestCase { public void testNoMaster() throws InterruptedException { final String index = "test"; - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); DiscoveryNodes.Builder noMasterBuilder = DiscoveryNodes.builder(clusterService.state().nodes()); noMasterBuilder.masterNodeId(null); @@ -207,7 +206,7 @@ public class ShardStateActionTests extends ESTestCase { public void testMasterChannelException() throws InterruptedException { final String index = "test"; - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); @@ -264,7 +263,7 @@ public class ShardStateActionTests extends ESTestCase { public void testUnhandledFailure() { final String index = "test"; - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); @@ -294,7 +293,7 @@ public class ShardStateActionTests extends ESTestCase { public void testShardNotFound() throws InterruptedException { final String index = "test"; - clusterService.setState(stateWithStartedPrimary(index, true, randomInt(5))); + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); diff --git a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 9a4e6a814a3..5c93bbb7d5d 100644 --- a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -57,6 +57,8 @@ import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.lease.Releasables; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.settings.Settings; @@ -108,6 +110,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -125,6 +128,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitC import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; /** * Simple unit-test IndexShard related operations. @@ -316,36 +320,41 @@ public class IndexShardTests extends ESSingleNodeTestCase { } - public void testDeleteIndexDecreasesCounter() throws InterruptedException, ExecutionException, IOException { + public void testDeleteIndexPreventsNewOperations() throws InterruptedException, ExecutionException, IOException { assertAcked(client().admin().indices().prepareCreate("test").setSettings(Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)).get()); ensureGreen("test"); IndicesService indicesService = getInstanceFromNode(IndicesService.class); IndexService indexService = indicesService.indexServiceSafe("test"); IndexShard indexShard = indexService.getShardOrNull(0); client().admin().indices().prepareDelete("test").get(); - assertThat(indexShard.getOperationsCount(), equalTo(0)); + assertThat(indexShard.getActiveOperationsCount(), equalTo(0)); try { - indexShard.incrementOperationCounter(); + indexShard.acquirePrimaryOperationLock(); + fail("we should not be able to increment anymore"); + } catch (IndexShardClosedException e) { + // expected + } + try { + indexShard.acquireReplicaOperationLock(); fail("we should not be able to increment anymore"); } catch (IndexShardClosedException e) { // expected } } - public void testIndexShardCounter() throws InterruptedException, ExecutionException, IOException { + public void testIndexOperationsCounter() throws InterruptedException, ExecutionException, IOException { assertAcked(client().admin().indices().prepareCreate("test").setSettings(Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)).get()); ensureGreen("test"); IndicesService indicesService = getInstanceFromNode(IndicesService.class); IndexService indexService = indicesService.indexServiceSafe("test"); IndexShard indexShard = indexService.getShardOrNull(0); - assertEquals(0, indexShard.getOperationsCount()); - indexShard.incrementOperationCounter(); - assertEquals(1, indexShard.getOperationsCount()); - indexShard.incrementOperationCounter(); - assertEquals(2, indexShard.getOperationsCount()); - indexShard.decrementOperationCounter(); - indexShard.decrementOperationCounter(); - assertEquals(0, indexShard.getOperationsCount()); + assertEquals(0, indexShard.getActiveOperationsCount()); + Releasable operation1 = indexShard.acquirePrimaryOperationLock(); + assertEquals(1, indexShard.getActiveOperationsCount()); + Releasable operation2 = indexShard.acquirePrimaryOperationLock(); + assertEquals(2, indexShard.getActiveOperationsCount()); + Releasables.close(operation1, operation2); + assertEquals(0, indexShard.getActiveOperationsCount()); } public void testMarkAsInactiveTriggersSyncedFlush() throws Exception { @@ -777,6 +786,89 @@ public class IndexShardTests extends ESSingleNodeTestCase { assertEquals(total + 1, shard.flushStats().getTotal()); } + public void testLockingBeforeAndAfterRelocated() throws Exception { + assertAcked(client().admin().indices().prepareCreate("test").setSettings( + Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0) + ).get()); + ensureGreen(); + IndicesService indicesService = getInstanceFromNode(IndicesService.class); + IndexService test = indicesService.indexService("test"); + final IndexShard shard = test.getShardOrNull(0); + CountDownLatch latch = new CountDownLatch(1); + Thread recoveryThread = new Thread(() -> { + latch.countDown(); + shard.relocated("simulated recovery"); + }); + + try (Releasable ignored = shard.acquirePrimaryOperationLock()) { + // start finalization of recovery + recoveryThread.start(); + latch.await(); + // recovery can only be finalized after we release the current primaryOperationLock + assertThat(shard.state(), equalTo(IndexShardState.STARTED)); + } + // recovery can be now finalized + recoveryThread.join(); + assertThat(shard.state(), equalTo(IndexShardState.RELOCATED)); + try (Releasable ignored = shard.acquirePrimaryOperationLock()) { + // lock can again be acquired + assertThat(shard.state(), equalTo(IndexShardState.RELOCATED)); + } + } + + public void testStressRelocated() throws Exception { + assertAcked(client().admin().indices().prepareCreate("test").setSettings( + Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0) + ).get()); + ensureGreen(); + IndicesService indicesService = getInstanceFromNode(IndicesService.class); + IndexService test = indicesService.indexService("test"); + final IndexShard shard = test.getShardOrNull(0); + final int numThreads = randomIntBetween(2, 4); + Thread[] indexThreads = new Thread[numThreads]; + CountDownLatch somePrimaryOperationLockAcquired = new CountDownLatch(1); + CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); + for (int i = 0; i < indexThreads.length; i++) { + indexThreads[i] = new Thread() { + @Override + public void run() { + try (Releasable operationLock = shard.acquirePrimaryOperationLock()) { + somePrimaryOperationLockAcquired.countDown(); + barrier.await(); + } catch (InterruptedException | BrokenBarrierException e) { + throw new RuntimeException(e); + } + } + }; + indexThreads[i].start(); + } + AtomicBoolean relocated = new AtomicBoolean(); + final Thread recoveryThread = new Thread(() -> { + shard.relocated("simulated recovery"); + relocated.set(true); + }); + // ensure we wait for at least one primary operation lock to be acquired + somePrimaryOperationLockAcquired.await(); + // start recovery thread + recoveryThread.start(); + assertThat(relocated.get(), equalTo(false)); + assertThat(shard.getActiveOperationsCount(), greaterThan(0)); + // ensure we only transition to RELOCATED state after pending operations completed + assertThat(shard.state(), equalTo(IndexShardState.STARTED)); + // complete pending operations + barrier.await(); + // complete recovery/relocation + recoveryThread.join(); + // ensure relocated successfully once pending operations are done + assertThat(relocated.get(), equalTo(true)); + assertThat(shard.state(), equalTo(IndexShardState.RELOCATED)); + assertThat(shard.getActiveOperationsCount(), equalTo(0)); + + for (Thread indexThread : indexThreads) { + indexThread.join(); + } + } + public void testRecoverFromStore() throws IOException { createIndex("test"); ensureGreen(); @@ -857,6 +949,27 @@ public class IndexShardTests extends ESSingleNodeTestCase { assertHitCount(client().prepareSearch().get(), 1); } + public void testRecoveryFailsAfterMovingToRelocatedState() throws InterruptedException { + createIndex("test"); + ensureGreen(); + IndicesService indicesService = getInstanceFromNode(IndicesService.class); + IndexService test = indicesService.indexService("test"); + final IndexShard shard = test.getShardOrNull(0); + ShardRouting origRouting = shard.routingEntry(); + assertThat(shard.state(), equalTo(IndexShardState.STARTED)); + ShardRouting inRecoveryRouting = new ShardRouting(origRouting); + ShardRoutingHelper.relocate(inRecoveryRouting, "some_node"); + shard.updateRoutingEntry(inRecoveryRouting, true); + shard.relocated("simulate mark as relocated"); + assertThat(shard.state(), equalTo(IndexShardState.RELOCATED)); + ShardRouting failedRecoveryRouting = new ShardRouting(origRouting); + try { + shard.updateRoutingEntry(failedRecoveryRouting, true); + fail("Expected IndexShardRelocatedException"); + } catch (IndexShardRelocatedException expected) { + } + } + public void testRestoreShard() throws IOException { createIndex("test"); createIndex("test_target"); diff --git a/core/src/test/java/org/elasticsearch/indices/IndicesLifecycleListenerIT.java b/core/src/test/java/org/elasticsearch/indices/IndicesLifecycleListenerIT.java index f1f8a8222cb..b074729cdff 100644 --- a/core/src/test/java/org/elasticsearch/indices/IndicesLifecycleListenerIT.java +++ b/core/src/test/java/org/elasticsearch/indices/IndicesLifecycleListenerIT.java @@ -58,6 +58,7 @@ import static org.elasticsearch.index.shard.IndexShardState.CLOSED; import static org.elasticsearch.index.shard.IndexShardState.CREATED; import static org.elasticsearch.index.shard.IndexShardState.POST_RECOVERY; import static org.elasticsearch.index.shard.IndexShardState.RECOVERING; +import static org.elasticsearch.index.shard.IndexShardState.RELOCATED; import static org.elasticsearch.index.shard.IndexShardState.STARTED; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.CoreMatchers.equalTo; @@ -181,7 +182,7 @@ public class IndicesLifecycleListenerIT extends ESIntegTestCase { ensureGreen(); //the 3 relocated shards get closed on the first node - assertShardStatesMatch(stateChangeListenerNode1, 3, CLOSED); + assertShardStatesMatch(stateChangeListenerNode1, 3, RELOCATED, CLOSED); //the 3 relocated shards get created on the second node assertShardStatesMatch(stateChangeListenerNode2, 3, CREATED, RECOVERING, POST_RECOVERY, STARTED); diff --git a/core/src/test/java/org/elasticsearch/indices/flush/SyncedFlushSingleNodeTests.java b/core/src/test/java/org/elasticsearch/indices/flush/SyncedFlushSingleNodeTests.java index c30a5adaaca..239cb7a9096 100644 --- a/core/src/test/java/org/elasticsearch/indices/flush/SyncedFlushSingleNodeTests.java +++ b/core/src/test/java/org/elasticsearch/indices/flush/SyncedFlushSingleNodeTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.shard.IndexShard; @@ -110,8 +111,7 @@ public class SyncedFlushSingleNodeTests extends ESSingleNodeTestCase { SyncedFlushService flushService = getInstanceFromNode(SyncedFlushService.class); final ShardId shardId = shard.shardId(); - shard.incrementOperationCounter(); - try { + try (Releasable operationLock = shard.acquirePrimaryOperationLock()) { SyncedFlushUtil.LatchedListener listener = new SyncedFlushUtil.LatchedListener<>(); flushService.attemptSyncedFlush(shardId, listener); listener.latch.await(); @@ -121,8 +121,6 @@ public class SyncedFlushSingleNodeTests extends ESSingleNodeTestCase { assertEquals(0, syncedFlushResult.successfulShards()); assertNotEquals(0, syncedFlushResult.totalShards()); assertEquals("[1] ongoing operations on primary", syncedFlushResult.failureReason()); - } finally { - shard.decrementOperationCounter(); } } diff --git a/core/src/test/java/org/elasticsearch/indices/recovery/IndexPrimaryRelocationIT.java b/core/src/test/java/org/elasticsearch/indices/recovery/IndexPrimaryRelocationIT.java new file mode 100644 index 00000000000..727641eb224 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/indices/recovery/IndexPrimaryRelocationIT.java @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.indices.recovery; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.delete.DeleteResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.routing.allocation.command.MoveAllocationCommand; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.junit.annotations.TestLogging; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.equalTo; + +@TestLogging("_root:DEBUG") +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST) +public class IndexPrimaryRelocationIT extends ESIntegTestCase { + + private static final int RELOCATION_COUNT = 25; + + public void testPrimaryRelocationWhileIndexing() throws Exception { + internalCluster().ensureAtLeastNumDataNodes(randomIntBetween(2, 3)); + client().admin().indices().prepareCreate("test") + .setSettings(Settings.settingsBuilder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)) + .addMapping("type", "field", "type=string") + .get(); + ensureGreen("test"); + + final AtomicBoolean finished = new AtomicBoolean(false); + Thread indexingThread = new Thread() { + @Override + public void run() { + while (finished.get() == false) { + IndexResponse indexResponse = client().prepareIndex("test", "type", "id").setSource("field", "value").get(); + assertThat("deleted document was found", indexResponse.isCreated(), equalTo(true)); + DeleteResponse deleteResponse = client().prepareDelete("test", "type", "id").get(); + assertThat("indexed document was not found", deleteResponse.isFound(), equalTo(true)); + } + } + }; + indexingThread.start(); + + ClusterState initialState = client().admin().cluster().prepareState().get().getState(); + DiscoveryNode[] dataNodes = initialState.getNodes().dataNodes().values().toArray(DiscoveryNode.class); + DiscoveryNode relocationSource = initialState.getNodes().dataNodes().get(initialState.getRoutingTable().shardRoutingTable("test", 0).primaryShard().currentNodeId()); + for (int i = 0; i < RELOCATION_COUNT; i++) { + DiscoveryNode relocationTarget = randomFrom(dataNodes); + while (relocationTarget.equals(relocationSource)) { + relocationTarget = randomFrom(dataNodes); + } + logger.info("--> [iteration {}] relocating from {} to {} ", i, relocationSource.getName(), relocationTarget.getName()); + client().admin().cluster().prepareReroute() + .add(new MoveAllocationCommand("test", 0, relocationSource.getId(), relocationTarget.getId())) + .execute().actionGet(); + ClusterHealthResponse clusterHealthResponse = client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForRelocatingShards(0).execute().actionGet(); + assertThat(clusterHealthResponse.isTimedOut(), equalTo(false)); + logger.info("--> [iteration {}] relocation complete", i); + relocationSource = relocationTarget; + if (indexingThread.isAlive() == false) { // indexing process aborted early, no need for more relocations as test has already failed + break; + } + + } + finished.set(true); + indexingThread.join(); + } +} diff --git a/core/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/core/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 12acea4f9ac..cc11cb82057 100644 --- a/core/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/core/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -286,7 +286,7 @@ public class IndexRecoveryIT extends ESIntegTestCase { assertRecoveryState(nodeARecoveryStates.get(0), 0, Type.STORE, Stage.DONE, nodeA, nodeA, false); validateIndexRecoveryState(nodeARecoveryStates.get(0).getIndex()); - assertOnGoingRecoveryState(nodeBRecoveryStates.get(0), 0, Type.RELOCATION, nodeA, nodeB, false); + assertOnGoingRecoveryState(nodeBRecoveryStates.get(0), 0, Type.PRIMARY_RELOCATION, nodeA, nodeB, false); validateIndexRecoveryState(nodeBRecoveryStates.get(0).getIndex()); logger.info("--> request node recovery stats"); @@ -339,7 +339,7 @@ public class IndexRecoveryIT extends ESIntegTestCase { recoveryStates = response.shardRecoveryStates().get(INDEX_NAME); assertThat(recoveryStates.size(), equalTo(1)); - assertRecoveryState(recoveryStates.get(0), 0, Type.RELOCATION, Stage.DONE, nodeA, nodeB, false); + assertRecoveryState(recoveryStates.get(0), 0, Type.PRIMARY_RELOCATION, Stage.DONE, nodeA, nodeB, false); validateIndexRecoveryState(recoveryStates.get(0).getIndex()); statsResponse = client().admin().cluster().prepareNodesStats().clear().setIndices(new CommonStatsFlags(CommonStatsFlags.Flag.Recovery)).get(); @@ -400,7 +400,7 @@ public class IndexRecoveryIT extends ESIntegTestCase { assertRecoveryState(nodeARecoveryStates.get(0), 0, Type.REPLICA, Stage.DONE, nodeB, nodeA, false); validateIndexRecoveryState(nodeARecoveryStates.get(0).getIndex()); - assertRecoveryState(nodeBRecoveryStates.get(0), 0, Type.RELOCATION, Stage.DONE, nodeA, nodeB, false); + assertRecoveryState(nodeBRecoveryStates.get(0), 0, Type.PRIMARY_RELOCATION, Stage.DONE, nodeA, nodeB, false); validateIndexRecoveryState(nodeBRecoveryStates.get(0).getIndex()); // relocations of replicas are marked as REPLICA and the source node is the node holding the primary (B) @@ -421,7 +421,7 @@ public class IndexRecoveryIT extends ESIntegTestCase { nodeCRecoveryStates = findRecoveriesForTargetNode(nodeC, recoveryStates); assertThat(nodeCRecoveryStates.size(), equalTo(1)); - assertRecoveryState(nodeBRecoveryStates.get(0), 0, Type.RELOCATION, Stage.DONE, nodeA, nodeB, false); + assertRecoveryState(nodeBRecoveryStates.get(0), 0, Type.PRIMARY_RELOCATION, Stage.DONE, nodeA, nodeB, false); validateIndexRecoveryState(nodeBRecoveryStates.get(0).getIndex()); // relocations of replicas are marked as REPLICA and the source node is the node holding the primary (B) @@ -503,7 +503,7 @@ public class IndexRecoveryIT extends ESIntegTestCase { final IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs]; for (int i = 0; i < numDocs; i++) { - docs[i] = client().prepareIndex(INDEX_NAME, INDEX_TYPE). + docs[i] = client().prepareIndex(name, INDEX_TYPE). setSource("foo-int", randomInt(), "foo-string", randomAsciiOfLength(32), "foo-float", randomFloat()); @@ -511,8 +511,8 @@ public class IndexRecoveryIT extends ESIntegTestCase { indexRandom(true, docs); flush(); - assertThat(client().prepareSearch(INDEX_NAME).setSize(0).get().getHits().totalHits(), equalTo((long) numDocs)); - return client().admin().indices().prepareStats(INDEX_NAME).execute().actionGet(); + assertThat(client().prepareSearch(name).setSize(0).get().getHits().totalHits(), equalTo((long) numDocs)); + return client().admin().indices().prepareStats(name).execute().actionGet(); } private void validateIndexRecoveryState(RecoveryState.Index indexState) { diff --git a/core/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/core/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index c8cad5be296..b29404d59b6 100644 --- a/core/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/core/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -69,7 +69,7 @@ public class RecoverySourceHandlerTests extends ESTestCase { StartRecoveryRequest request = new StartRecoveryRequest(shardId, new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), - randomBoolean(), null, RecoveryState.Type.STORE, randomLong()); + null, RecoveryState.Type.STORE, randomLong()); Store store = newStore(createTempDir()); RecoverySourceHandler handler = new RecoverySourceHandler(null, request, recoverySettings, null, logger); Directory dir = store.directory(); @@ -118,7 +118,7 @@ public class RecoverySourceHandlerTests extends ESTestCase { StartRecoveryRequest request = new StartRecoveryRequest(shardId, new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), - randomBoolean(), null, RecoveryState.Type.STORE, randomLong()); + null, RecoveryState.Type.STORE, randomLong()); Path tempDir = createTempDir(); Store store = newStore(tempDir, false); AtomicBoolean failedEngine = new AtomicBoolean(false); @@ -181,7 +181,7 @@ public class RecoverySourceHandlerTests extends ESTestCase { StartRecoveryRequest request = new StartRecoveryRequest(shardId, new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), new DiscoveryNode("b", DummyTransportAddress.INSTANCE, Version.CURRENT), - randomBoolean(), null, RecoveryState.Type.STORE, randomLong()); + null, RecoveryState.Type.STORE, randomLong()); Path tempDir = createTempDir(); Store store = newStore(tempDir, false); AtomicBoolean failedEngine = new AtomicBoolean(false); diff --git a/core/src/test/java/org/elasticsearch/indices/recovery/StartRecoveryRequestTests.java b/core/src/test/java/org/elasticsearch/indices/recovery/StartRecoveryRequestTests.java index c7a7852e426..3406388bd5b 100644 --- a/core/src/test/java/org/elasticsearch/indices/recovery/StartRecoveryRequestTests.java +++ b/core/src/test/java/org/elasticsearch/indices/recovery/StartRecoveryRequestTests.java @@ -43,11 +43,9 @@ public class StartRecoveryRequestTests extends ESTestCase { new ShardId("test", "_na_", 0), new DiscoveryNode("a", new LocalTransportAddress("1"), targetNodeVersion), new DiscoveryNode("b", new LocalTransportAddress("1"), targetNodeVersion), - true, Store.MetadataSnapshot.EMPTY, - RecoveryState.Type.RELOCATION, + RecoveryState.Type.PRIMARY_RELOCATION, 1L - ); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); OutputStreamStreamOutput out = new OutputStreamStreamOutput(outBuffer); @@ -63,7 +61,6 @@ public class StartRecoveryRequestTests extends ESTestCase { assertThat(outRequest.shardId(), equalTo(inRequest.shardId())); assertThat(outRequest.sourceNode(), equalTo(inRequest.sourceNode())); assertThat(outRequest.targetNode(), equalTo(inRequest.targetNode())); - assertThat(outRequest.markAsRelocated(), equalTo(inRequest.markAsRelocated())); assertThat(outRequest.metadataSnapshot().asMap(), equalTo(inRequest.metadataSnapshot().asMap())); assertThat(outRequest.recoveryId(), equalTo(inRequest.recoveryId())); assertThat(outRequest.recoveryType(), equalTo(inRequest.recoveryType())); diff --git a/core/src/test/java/org/elasticsearch/recovery/FullRollingRestartIT.java b/core/src/test/java/org/elasticsearch/recovery/FullRollingRestartIT.java index 61dca3f37af..8c6b71c9eac 100644 --- a/core/src/test/java/org/elasticsearch/recovery/FullRollingRestartIT.java +++ b/core/src/test/java/org/elasticsearch/recovery/FullRollingRestartIT.java @@ -151,7 +151,7 @@ public class FullRollingRestartIT extends ESIntegTestCase { ClusterState state = client().admin().cluster().prepareState().get().getState(); RecoveryResponse recoveryResponse = client().admin().indices().prepareRecoveries("test").get(); for (RecoveryState recoveryState : recoveryResponse.shardRecoveryStates().get("test")) { - assertTrue("relocated from: " + recoveryState.getSourceNode() + " to: " + recoveryState.getTargetNode() + "\n" + state.prettyPrint(), recoveryState.getType() != RecoveryState.Type.RELOCATION); + assertTrue("relocated from: " + recoveryState.getSourceNode() + " to: " + recoveryState.getTargetNode() + "\n" + state.prettyPrint(), recoveryState.getType() != RecoveryState.Type.PRIMARY_RELOCATION); } internalCluster().restartRandomDataNode(); ensureGreen(); @@ -159,7 +159,7 @@ public class FullRollingRestartIT extends ESIntegTestCase { recoveryResponse = client().admin().indices().prepareRecoveries("test").get(); for (RecoveryState recoveryState : recoveryResponse.shardRecoveryStates().get("test")) { - assertTrue("relocated from: " + recoveryState.getSourceNode() + " to: " + recoveryState.getTargetNode()+ "-- \nbefore: \n" + state.prettyPrint() + "\nafter: \n" + afterState.prettyPrint(), recoveryState.getType() != RecoveryState.Type.RELOCATION); + assertTrue("relocated from: " + recoveryState.getSourceNode() + " to: " + recoveryState.getTargetNode()+ "-- \nbefore: \n" + state.prettyPrint() + "\nafter: \n" + afterState.prettyPrint(), recoveryState.getType() != RecoveryState.Type.PRIMARY_RELOCATION); } } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 658264864e0..d9f634d503e 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -1036,7 +1036,7 @@ public final class InternalTestCluster extends TestCluster { IndicesService indexServices = getInstance(IndicesService.class, nodeAndClient.name); for (IndexService indexService : indexServices) { for (IndexShard indexShard : indexService) { - assertThat("index shard counter on shard " + indexShard.shardId() + " on node " + nodeAndClient.name + " not 0", indexShard.getOperationsCount(), equalTo(0)); + assertThat("index shard counter on shard " + indexShard.shardId() + " on node " + nodeAndClient.name + " not 0", indexShard.getActiveOperationsCount(), equalTo(0)); } } } From a7fb5a27cc5e822d71dafde172a8398f63388c62 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 2 Feb 2016 11:12:58 +0100 Subject: [PATCH 17/49] Mute IndexShardTests.testStressRelocated --- .../test/java/org/elasticsearch/index/shard/IndexShardTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 5c93bbb7d5d..bedda8d3c61 100644 --- a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -816,6 +816,7 @@ public class IndexShardTests extends ESSingleNodeTestCase { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/16364") public void testStressRelocated() throws Exception { assertAcked(client().admin().indices().prepareCreate("test").setSettings( Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0) From c108a4ce6d32f43397d5adbcb4b930f5be174d48 Mon Sep 17 00:00:00 2001 From: Isabel Drost-Fromm Date: Mon, 7 Dec 2015 17:16:55 +0100 Subject: [PATCH 18/49] Make GeoDistanceSortBuilder serializable Adds to GeoDistanceSortBuilder: * equals * hashcode * writeto/readfrom * moves xcontent parsing logic over * adds roundtrip tests * fixes roundtrip test for xcontent by keeping points just as geopoints not geohashes internally * fixes xcontent parsing of ignore_malformed if coerce is set/unset * adds exception to sortMode setter to avoid setting invalid sort modes Relates to #15178 --- .../search/sort/GeoDistanceSortBuilder.java | 384 ++++++++++++++++-- .../search/sort/GeoDistanceSortParser.java | 28 +- .../search/sort/SortBuilders.java | 30 +- .../search/sort/SortElementParserTemp.java | 40 ++ .../elasticsearch/search/sort/SortOrder.java | 3 +- .../builder/SearchSourceBuilderTests.java | 4 +- .../search/sort/AbstractSortTestCase.java | 162 ++++++++ .../sort/GeoDistanceSortBuilderTests.java | 251 ++++++++++++ .../search/sort/RandomSortDataGenerator.java | 113 ++++++ .../messy/tests/GeoDistanceTests.java | 42 +- .../messy/tests/SimpleSortTests.java | 36 +- 11 files changed, 993 insertions(+), 100 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java create mode 100644 core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java create mode 100644 core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java create mode 100644 core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index 8f502e53058..428f81dba73 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -22,41 +22,114 @@ package org.elasticsearch.search.sort; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.search.MultiValueMode; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Objects; /** * A geo distance based sorting on a geo point like field. */ -public class GeoDistanceSortBuilder extends SortBuilder { +public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, NamedWriteable, SortElementParserTemp { + public static final String NAME = "_geo_distance"; + public static final boolean DEFAULT_COERCE = false; + public static final boolean DEFAULT_IGNORE_MALFORMED = false; - final String fieldName; + static final GeoDistanceSortBuilder PROTOTYPE = new GeoDistanceSortBuilder("", -1, -1); + + private final String fieldName; private final List points = new ArrayList<>(); - private final List geohashes = new ArrayList<>(); - private GeoDistance geoDistance; - private DistanceUnit unit; - private SortOrder order; - private String sortMode; + private GeoDistance geoDistance = GeoDistance.DEFAULT; + private DistanceUnit unit = DistanceUnit.DEFAULT; + private SortOrder order = SortOrder.ASC; + + // TODO there is an enum that covers that parameter which we should be using here + private String sortMode = null; + @SuppressWarnings("rawtypes") private QueryBuilder nestedFilter; private String nestedPath; - private Boolean coerce; - private Boolean ignoreMalformed; + + // TODO switch to GeoValidationMethod enum + private boolean coerce = DEFAULT_COERCE; + private boolean ignoreMalformed = DEFAULT_IGNORE_MALFORMED; /** * Constructs a new distance based sort on a geo point like field. * * @param fieldName The geo point like field name. + * @param points The points to create the range distance facets from. */ - public GeoDistanceSortBuilder(String fieldName) { + public GeoDistanceSortBuilder(String fieldName, GeoPoint... points) { this.fieldName = fieldName; + if (points.length == 0) { + throw new IllegalArgumentException("Geo distance sorting needs at least one point."); + } + this.points.addAll(Arrays.asList(points)); + } + + /** + * Constructs a new distance based sort on a geo point like field. + * + * @param fieldName The geo point like field name. + * @param lat Latitude of the point to create the range distance facets from. + * @param lon Longitude of the point to create the range distance facets from. + */ + public GeoDistanceSortBuilder(String fieldName, double lat, double lon) { + this(fieldName, new GeoPoint(lat, lon)); + } + + /** + * Constructs a new distance based sort on a geo point like field. + * + * @param fieldName The geo point like field name. + * @param geohashes The points to create the range distance facets from. + */ + public GeoDistanceSortBuilder(String fieldName, String ... geohashes) { + if (geohashes.length == 0) { + throw new IllegalArgumentException("Geo distance sorting needs at least one point."); + } + for (String geohash : geohashes) { + this.points.add(GeoPoint.fromGeohash(geohash)); + } + this.fieldName = fieldName; + } + + /** + * Copy constructor. + * */ + GeoDistanceSortBuilder(GeoDistanceSortBuilder original) { + this.fieldName = original.fieldName(); + this.points.addAll(original.points); + this.geoDistance = original.geoDistance; + this.unit = original.unit; + this.order = original.order; + this.sortMode = original.sortMode; + this.nestedFilter = original.nestedFilter; + this.nestedPath = original.nestedPath; + this.coerce = original.coerce; + this.ignoreMalformed = original.ignoreMalformed; + } + + /** + * Returns the geo point like field the distance based sort operates on. + * */ + public String fieldName() { + return this.fieldName; } /** @@ -79,15 +152,27 @@ public class GeoDistanceSortBuilder extends SortBuilder { this.points.addAll(Arrays.asList(points)); return this; } + + /** + * Returns the points to create the range distance facets from. + */ + public GeoPoint[] points() { + return this.points.toArray(new GeoPoint[this.points.size()]); + } /** * The geohash of the geo point to create the range distance facets from. + * + * Deprecated - please use points(GeoPoint... points) instead. */ + @Deprecated public GeoDistanceSortBuilder geohashes(String... geohashes) { - this.geohashes.addAll(Arrays.asList(geohashes)); + for (String geohash : geohashes) { + this.points.add(GeoPoint.fromGeohash(geohash)); + } return this; } - + /** * The geo distance type used to compute the distance. */ @@ -95,6 +180,13 @@ public class GeoDistanceSortBuilder extends SortBuilder { this.geoDistance = geoDistance; return this; } + + /** + * Returns the geo distance type used to compute the distance. + */ + public GeoDistance geoDistance() { + return this.geoDistance; + } /** * The distance unit to use. Defaults to {@link org.elasticsearch.common.unit.DistanceUnit#KILOMETERS} @@ -104,6 +196,13 @@ public class GeoDistanceSortBuilder extends SortBuilder { return this; } + /** + * Returns the distance unit to use. Defaults to {@link org.elasticsearch.common.unit.DistanceUnit#KILOMETERS} + */ + public DistanceUnit unit() { + return this.unit; + } + /** * The order of sorting. Defaults to {@link SortOrder#ASC}. */ @@ -113,11 +212,18 @@ public class GeoDistanceSortBuilder extends SortBuilder { return this; } + /** Returns the order of sorting. */ + public SortOrder order() { + return this.order; + } + /** * Not relevant. + * + * TODO should this throw an exception rather than silently ignore a parameter that is not used? */ @Override - public SortBuilder missing(Object missing) { + public GeoDistanceSortBuilder missing(Object missing) { return this; } @@ -126,10 +232,19 @@ public class GeoDistanceSortBuilder extends SortBuilder { * Possible values: min and max */ public GeoDistanceSortBuilder sortMode(String sortMode) { + MultiValueMode temp = MultiValueMode.fromString(sortMode); + if (temp == MultiValueMode.SUM) { + throw new IllegalArgumentException("sort_mode [sum] isn't supported for sorting by geo distance"); + } this.sortMode = sortMode; return this; } + /** Returns which distance to use for sorting in the case a document contains multiple geo points. */ + public String sortMode() { + return this.sortMode; + } + /** * Sets the nested filter that the nested objects should match with in order to be taken into account * for sorting. @@ -139,6 +254,14 @@ public class GeoDistanceSortBuilder extends SortBuilder { return this; } + /** + * Returns the nested filter that the nested objects should match with in order to be taken into account + * for sorting. + **/ + public QueryBuilder getNestedFilter() { + return this.nestedFilter; + } + /** * Sets the nested path if sorting occurs on a field that is inside a nested object. By default when sorting on a * field inside a nested object, the nearest upper nested object is selected as nested path. @@ -147,42 +270,53 @@ public class GeoDistanceSortBuilder extends SortBuilder { this.nestedPath = nestedPath; return this; } + + /** + * Returns the nested path if sorting occurs on a field that is inside a nested object. By default when sorting on a + * field inside a nested object, the nearest upper nested object is selected as nested path. + */ + public String getNestedPath() { + return this.nestedPath; + } public GeoDistanceSortBuilder coerce(boolean coerce) { this.coerce = coerce; return this; } + public boolean coerce() { + return this.coerce; + } + public GeoDistanceSortBuilder ignoreMalformed(boolean ignoreMalformed) { - this.ignoreMalformed = ignoreMalformed; + if (coerce == false) { + this.ignoreMalformed = ignoreMalformed; + } return this; } + + public boolean ignoreMalformed() { + return this.ignoreMalformed; + } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("_geo_distance"); - if (geohashes.size() == 0 && points.size() == 0) { - throw new ElasticsearchParseException("No points provided for _geo_distance sort."); - } + builder.startObject(NAME); builder.startArray(fieldName); for (GeoPoint point : points) { builder.value(point); } - for (String geohash : geohashes) { - builder.value(geohash); - } builder.endArray(); - if (unit != null) { - builder.field("unit", unit); - } - if (geoDistance != null) { - builder.field("distance_type", geoDistance.name().toLowerCase(Locale.ROOT)); - } + builder.field("unit", unit); + builder.field("distance_type", geoDistance.name().toLowerCase(Locale.ROOT)); if (order == SortOrder.DESC) { builder.field("reverse", true); + } else { + builder.field("reverse", false); } + if (sortMode != null) { builder.field("mode", sortMode); } @@ -193,14 +327,198 @@ public class GeoDistanceSortBuilder extends SortBuilder { if (nestedFilter != null) { builder.field("nested_filter", nestedFilter, params); } - if (coerce != null) { - builder.field("coerce", coerce); - } - if (ignoreMalformed != null) { - builder.field("ignore_malformed", ignoreMalformed); - } + builder.field("coerce", coerce); + builder.field("ignore_malformed", ignoreMalformed); builder.endObject(); return builder; } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + GeoDistanceSortBuilder other = (GeoDistanceSortBuilder) object; + return Objects.equals(fieldName, other.fieldName) && + Objects.deepEquals(points, other.points) && + Objects.equals(geoDistance, other.geoDistance) && + Objects.equals(unit, other.unit) && + Objects.equals(sortMode, other.sortMode) && + Objects.equals(order, other.order) && + Objects.equals(nestedFilter, other.nestedFilter) && + Objects.equals(nestedPath, other.nestedPath) && + Objects.equals(coerce, other.coerce) && + Objects.equals(ignoreMalformed, other.ignoreMalformed); + } + + @Override + public int hashCode() { + return Objects.hash(this.fieldName, this.points, this.geoDistance, + this.unit, this.sortMode, this.order, this.nestedFilter, this.nestedPath, this.coerce, this.ignoreMalformed); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(fieldName); + out.writeGenericValue(points); + + geoDistance.writeTo(out); + unit.writeTo(out); + order.writeTo(out); + out.writeOptionalString(sortMode); + if (nestedFilter != null) { + out.writeBoolean(true); + out.writeQuery(nestedFilter); + } else { + out.writeBoolean(false); + } + out.writeOptionalString(nestedPath); + out.writeBoolean(coerce); + out.writeBoolean(ignoreMalformed); + } + + @Override + public GeoDistanceSortBuilder readFrom(StreamInput in) throws IOException { + String fieldName = in.readString(); + + ArrayList points = (ArrayList) in.readGenericValue(); + GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(fieldName, points.toArray(new GeoPoint[points.size()])); + + result.geoDistance(GeoDistance.readGeoDistanceFrom(in)); + result.unit(DistanceUnit.readDistanceUnit(in)); + result.order(SortOrder.readOrderFrom(in)); + String sortMode = in.readOptionalString(); + if (sortMode != null) { + result.sortMode(sortMode); + } + if (in.readBoolean()) { + result.setNestedFilter(in.readQuery()); + } + result.setNestedPath(in.readOptionalString()); + result.coerce(in.readBoolean()); + result.ignoreMalformed(in.readBoolean()); + return result; + } + + @Override + public GeoDistanceSortBuilder fromXContent(QueryParseContext context, String elementName) throws IOException { + XContentParser parser = context.parser(); + String fieldName = null; + List geoPoints = new ArrayList<>(); + DistanceUnit unit = DistanceUnit.DEFAULT; + GeoDistance geoDistance = GeoDistance.DEFAULT; + boolean reverse = false; + MultiValueMode sortMode = null; + QueryBuilder nestedFilter = null; + String nestedPath = null; + + boolean coerce = GeoDistanceSortBuilder.DEFAULT_COERCE; + boolean ignoreMalformed = GeoDistanceSortBuilder.DEFAULT_IGNORE_MALFORMED; + + XContentParser.Token token; + String currentName = parser.currentName(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentName = parser.currentName(); + } else if (token == XContentParser.Token.START_ARRAY) { + parseGeoPoints(parser, geoPoints); + + fieldName = currentName; + } else if (token == XContentParser.Token.START_OBJECT) { + // the json in the format of -> field : { lat : 30, lon : 12 } + if ("nested_filter".equals(currentName) || "nestedFilter".equals(currentName)) { + // TODO Note to remember: while this is kept as a QueryBuilder internally, + // we need to make sure to call toFilter() on it once on the shard + // (e.g. in the new build() method) + nestedFilter = context.parseInnerQueryBuilder(); + } else { + fieldName = currentName; + GeoPoint point = new GeoPoint(); + GeoUtils.parseGeoPoint(parser, point); + geoPoints.add(point); + } + } else if (token.isValue()) { + if ("reverse".equals(currentName)) { + reverse = parser.booleanValue(); + } else if ("order".equals(currentName)) { + reverse = "desc".equals(parser.text()); + } else if ("unit".equals(currentName)) { + unit = DistanceUnit.fromString(parser.text()); + } else if ("distance_type".equals(currentName) || "distanceType".equals(currentName)) { + geoDistance = GeoDistance.fromString(parser.text()); + } else if ("coerce".equals(currentName) || "normalize".equals(currentName)) { + coerce = parser.booleanValue(); + if (coerce == true) { + ignoreMalformed = true; + } + } else if ("ignore_malformed".equals(currentName)) { + boolean ignore_malformed_value = parser.booleanValue(); + if (coerce == false) { + ignoreMalformed = ignore_malformed_value; + } + } else if ("sort_mode".equals(currentName) || "sortMode".equals(currentName) || "mode".equals(currentName)) { + sortMode = MultiValueMode.fromString(parser.text()); + } else if ("nested_path".equals(currentName) || "nestedPath".equals(currentName)) { + nestedPath = parser.text(); + } else { + GeoPoint point = new GeoPoint(); + point.resetFromString(parser.text()); + geoPoints.add(point); + fieldName = currentName; + } + } + } + + GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(fieldName, geoPoints.toArray(new GeoPoint[geoPoints.size()])); + result.geoDistance(geoDistance); + result.unit(unit); + if (reverse) { + result.order(SortOrder.DESC); + } else { + result.order(SortOrder.ASC); + } + if (sortMode != null) { + result.sortMode(sortMode.name()); + } + result.setNestedFilter(nestedFilter); + result.setNestedPath(nestedPath); + result.coerce(coerce); + result.ignoreMalformed(ignoreMalformed); + return result; + + } + + static void parseGeoPoints(XContentParser parser, List geoPoints) throws IOException { + while (!parser.nextToken().equals(XContentParser.Token.END_ARRAY)) { + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + // we might get here if the geo point is " number, number] " and the parser already moved over the opening bracket + // in this case we cannot use GeoUtils.parseGeoPoint(..) because this expects an opening bracket + double lon = parser.doubleValue(); + parser.nextToken(); + if (!parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)) { + throw new ElasticsearchParseException("geo point parsing: expected second number but got [{}] instead", parser.currentToken()); + } + double lat = parser.doubleValue(); + GeoPoint point = new GeoPoint(); + point.reset(lat, lon); + geoPoints.add(point); + } else { + GeoPoint point = new GeoPoint(); + GeoUtils.parseGeoPoint(parser, point); + geoPoints.add(point); + } + + } + } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java index 9fddf590ca4..248a051021e 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java @@ -72,8 +72,8 @@ public class GeoDistanceSortParser implements SortParser { NestedInnerQueryParseSupport nestedHelper = null; final boolean indexCreatedBeforeV2_0 = context.indexShard().getIndexSettings().getIndexVersionCreated().before(Version.V_2_0_0); - boolean coerce = false; - boolean ignoreMalformed = false; + boolean coerce = GeoDistanceSortBuilder.DEFAULT_COERCE; + boolean ignoreMalformed = GeoDistanceSortBuilder.DEFAULT_IGNORE_MALFORMED; XContentParser.Token token; String currentName = parser.currentName(); @@ -81,7 +81,7 @@ public class GeoDistanceSortParser implements SortParser { if (token == XContentParser.Token.FIELD_NAME) { currentName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { - parseGeoPoints(parser, geoPoints); + GeoDistanceSortBuilder.parseGeoPoints(parser, geoPoints); fieldName = currentName; } else if (token == XContentParser.Token.START_OBJECT) { @@ -213,26 +213,4 @@ public class GeoDistanceSortParser implements SortParser { return new SortField(fieldName, geoDistanceComparatorSource, reverse); } - private void parseGeoPoints(XContentParser parser, List geoPoints) throws IOException { - while (!parser.nextToken().equals(XContentParser.Token.END_ARRAY)) { - if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { - // we might get here if the geo point is " number, number] " and the parser already moved over the opening bracket - // in this case we cannot use GeoUtils.parseGeoPoint(..) because this expects an opening bracket - double lon = parser.doubleValue(); - parser.nextToken(); - if (!parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)) { - throw new ElasticsearchParseException("geo point parsing: expected second number but got [{}] instead", parser.currentToken()); - } - double lat = parser.doubleValue(); - GeoPoint point = new GeoPoint(); - point.reset(lat, lon); - geoPoints.add(point); - } else { - GeoPoint point = new GeoPoint(); - GeoUtils.parseGeoPoint(parser, point); - geoPoints.add(point); - } - - } - } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java b/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java index 9a843c43f74..f326fee3837 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java +++ b/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java @@ -19,8 +19,11 @@ package org.elasticsearch.search.sort; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.script.Script; +import java.util.Arrays; + /** * A set of static factory methods for {@link SortBuilder}s. * @@ -58,8 +61,31 @@ public class SortBuilders { * A geo distance based sort. * * @param fieldName The geo point like field name. + * @param lat Latitude of the point to create the range distance facets from. + * @param lon Longitude of the point to create the range distance facets from. + * */ - public static GeoDistanceSortBuilder geoDistanceSort(String fieldName) { - return new GeoDistanceSortBuilder(fieldName); + public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, double lat, double lon) { + return new GeoDistanceSortBuilder(fieldName, lat, lon); } + + /** + * Constructs a new distance based sort on a geo point like field. + * + * @param fieldName The geo point like field name. + * @param points The points to create the range distance facets from. + */ + public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, GeoPoint... points) { + return new GeoDistanceSortBuilder(fieldName, points); + } + + /** + * Constructs a new distance based sort on a geo point like field. + * + * @param fieldName The geo point like field name. + * @param geohashes The points to create the range distance facets from. + */ + public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, String ... geohashes) { + return new GeoDistanceSortBuilder(fieldName, geohashes); + } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java b/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java new file mode 100644 index 00000000000..8893471b6c1 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.sort; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.index.query.QueryParseContext; + +import java.io.IOException; + +// TODO once sort refactoring is done this needs to be merged into SortBuilder +public interface SortElementParserTemp { + /** + * Creates a new SortBuilder from the json held by the {@link SortElementParserTemp} + * in {@link org.elasticsearch.common.xcontent.XContent} format + * + * @param context + * the input parse context. The state on the parser contained in + * this context will be changed as a side effect of this method + * call + * @return the new item + */ + T fromXContent(QueryParseContext context, String elementName) throws IOException; +} diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java b/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java index 001924d1bdf..73e5ac55247 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java @@ -51,8 +51,7 @@ public enum SortOrder implements Writeable { } }; - public static final SortOrder DEFAULT = DESC; - private static final SortOrder PROTOTYPE = DEFAULT; + private static final SortOrder PROTOTYPE = ASC; @Override public SortOrder readFrom(StreamInput in) throws IOException { diff --git a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java index bb969b90de6..d414a64f60e 100644 --- a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java @@ -246,8 +246,8 @@ public class SearchSourceBuilderTests extends ESTestCase { builder.sort(SortBuilders.fieldSort(randomAsciiOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))); break; case 1: - builder.sort(SortBuilders.geoDistanceSort(randomAsciiOfLengthBetween(5, 20)) - .geohashes(AbstractQueryTestCase.randomGeohash(1, 12)).order(randomFrom(SortOrder.values()))); + builder.sort(SortBuilders.geoDistanceSort(randomAsciiOfLengthBetween(5, 20), + AbstractQueryTestCase.randomGeohash(1, 12)).order(randomFrom(SortOrder.values()))); break; case 2: builder.sort(SortBuilders.scoreSort().order(randomFrom(SortOrder.values()))); diff --git a/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java b/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java new file mode 100644 index 00000000000..dfea1a9316b --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java @@ -0,0 +1,162 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.sort; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.indices.query.IndicesQueriesRegistry; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.ESTestCase; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +public abstract class AbstractSortTestCase & ToXContent & SortElementParserTemp> extends ESTestCase { + + protected static NamedWriteableRegistry namedWriteableRegistry; + + private static final int NUMBER_OF_TESTBUILDERS = 20; + static IndicesQueriesRegistry indicesQueriesRegistry; + + @BeforeClass + public static void init() { + namedWriteableRegistry = new NamedWriteableRegistry(); + namedWriteableRegistry.registerPrototype(GeoDistanceSortBuilder.class, GeoDistanceSortBuilder.PROTOTYPE); + indicesQueriesRegistry = new SearchModule(Settings.EMPTY, namedWriteableRegistry).buildQueryParserRegistry(); + } + + @AfterClass + public static void afterClass() throws Exception { + namedWriteableRegistry = null; + } + + /** Returns random sort that is put under test */ + protected abstract T createTestItem(); + + /** Returns mutated version of original so the returned sort is different in terms of equals/hashcode */ + protected abstract T mutate(T original) throws IOException; + + /** + * Test that creates new sort from a random test sort and checks both for equality + */ + public void testFromXContent() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + T testItem = createTestItem(); + + XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); + if (randomBoolean()) { + builder.prettyPrint(); + } + builder.startObject(); + testItem.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + XContentParser itemParser = XContentHelper.createParser(builder.bytes()); + itemParser.nextToken(); + + /* + * filter out name of sort, or field name to sort on for element fieldSort + */ + itemParser.nextToken(); + String elementName = itemParser.currentName(); + itemParser.nextToken(); + + QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); + context.reset(itemParser); + NamedWriteable parsedItem = testItem.fromXContent(context, elementName); + assertNotSame(testItem, parsedItem); + assertEquals(testItem, parsedItem); + assertEquals(testItem.hashCode(), parsedItem.hashCode()); + } + } + + /** + * Test serialization and deserialization of the test sort. + */ + public void testSerialization() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + T testsort = createTestItem(); + T deserializedsort = copyItem(testsort); + assertEquals(testsort, deserializedsort); + assertEquals(testsort.hashCode(), deserializedsort.hashCode()); + assertNotSame(testsort, deserializedsort); + } + } + + /** + * Test equality and hashCode properties + */ + public void testEqualsAndHashcode() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { + T firstsort = createTestItem(); + assertFalse("sort is equal to null", firstsort.equals(null)); + assertFalse("sort is equal to incompatible type", firstsort.equals("")); + assertTrue("sort is not equal to self", firstsort.equals(firstsort)); + assertThat("same sort's hashcode returns different values if called multiple times", firstsort.hashCode(), + equalTo(firstsort.hashCode())); + assertThat("different sorts should not be equal", mutate(firstsort), not(equalTo(firstsort))); + assertThat("different sorts should have different hashcode", mutate(firstsort).hashCode(), not(equalTo(firstsort.hashCode()))); + + T secondsort = copyItem(firstsort); + assertTrue("sort is not equal to self", secondsort.equals(secondsort)); + assertTrue("sort is not equal to its copy", firstsort.equals(secondsort)); + assertTrue("equals is not symmetric", secondsort.equals(firstsort)); + assertThat("sort copy's hashcode is different from original hashcode", secondsort.hashCode(), equalTo(firstsort.hashCode())); + + T thirdsort = copyItem(secondsort); + assertTrue("sort is not equal to self", thirdsort.equals(thirdsort)); + assertTrue("sort is not equal to its copy", secondsort.equals(thirdsort)); + assertThat("sort copy's hashcode is different from original hashcode", secondsort.hashCode(), equalTo(thirdsort.hashCode())); + assertTrue("equals is not transitive", firstsort.equals(thirdsort)); + assertThat("sort copy's hashcode is different from original hashcode", firstsort.hashCode(), equalTo(thirdsort.hashCode())); + assertTrue("equals is not symmetric", thirdsort.equals(secondsort)); + assertTrue("equals is not symmetric", thirdsort.equals(firstsort)); + } + } + + protected T copyItem(T original) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + original.writeTo(output); + try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), namedWriteableRegistry)) { + @SuppressWarnings("unchecked") + T prototype = (T) namedWriteableRegistry.getPrototype(getPrototype(), original.getWriteableName()); + T copy = (T) prototype.readFrom(in); + return copy; + } + } + } + + protected abstract Class getPrototype(); +} diff --git a/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java b/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java new file mode 100644 index 00000000000..dbb473f22ef --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.sort; + + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.geo.GeoDistance; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.geo.RandomGeoGenerator; + +import java.io.IOException; +import java.util.Arrays; + +public class GeoDistanceSortBuilderTests extends AbstractSortTestCase { + + @Override + protected GeoDistanceSortBuilder createTestItem() { + String fieldName = randomAsciiOfLengthBetween(1, 10); + GeoDistanceSortBuilder result = null; + + int id = randomIntBetween(0, 2); + switch(id) { + case 0: + int count = randomIntBetween(1, 10); + String[] geohashes = new String[count]; + for (int i = 0; i < count; i++) { + geohashes[i] = RandomGeoGenerator.randomPoint(getRandom()).geohash(); + } + + result = new GeoDistanceSortBuilder(fieldName, geohashes); + break; + case 1: + GeoPoint pt = RandomGeoGenerator.randomPoint(getRandom()); + result = new GeoDistanceSortBuilder(fieldName, pt.getLat(), pt.getLon()); + break; + case 2: + result = new GeoDistanceSortBuilder(fieldName, points(new GeoPoint[0])); + break; + default: + throw new IllegalStateException("one of three geo initialisation strategies must be used"); + + } + if (randomBoolean()) { + result.geoDistance(geoDistance(result.geoDistance())); + } + if (randomBoolean()) { + result.unit(unit(result.unit())); + } + if (randomBoolean()) { + result.order(RandomSortDataGenerator.order(result.order())); + } + if (randomBoolean()) { + result.sortMode(mode(result.sortMode())); + } + if (randomBoolean()) { + result.setNestedFilter(RandomSortDataGenerator.nestedFilter(result.getNestedFilter())); + } + if (randomBoolean()) { + result.setNestedPath(RandomSortDataGenerator.randomAscii(result.getNestedPath())); + } + if (randomBoolean()) { + result.coerce(! result.coerce()); + } + if (randomBoolean()) { + result.ignoreMalformed(! result.ignoreMalformed()); + } + + return result; + } + + private static String mode(String original) { + String[] modes = {"MIN", "MAX", "AVG"}; + String mode = ESTestCase.randomFrom(modes); + while (mode.equals(original)) { + mode = ESTestCase.randomFrom(modes); + } + return mode; + } + + private DistanceUnit unit(DistanceUnit original) { + int id = -1; + while (id == -1 || (original != null && original.ordinal() == id)) { + id = randomIntBetween(0, DistanceUnit.values().length - 1); + } + return DistanceUnit.values()[id]; + } + + private GeoPoint[] points(GeoPoint[] original) { + GeoPoint[] result = null; + while (result == null || Arrays.deepEquals(original, result)) { + int count = randomIntBetween(1, 10); + result = new GeoPoint[count]; + for (int i = 0; i < count; i++) { + result[i] = RandomGeoGenerator.randomPoint(getRandom()); + } + } + return result; + } + + private GeoDistance geoDistance(GeoDistance original) { + int id = -1; + while (id == -1 || (original != null && original.ordinal() == id)) { + id = randomIntBetween(0, GeoDistance.values().length - 1); + } + return GeoDistance.values()[id]; + } + + @Override + protected GeoDistanceSortBuilder mutate(GeoDistanceSortBuilder original) throws IOException { + GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(original); + int parameter = randomIntBetween(0, 9); + switch (parameter) { + case 0: + while (Arrays.deepEquals(original.points(), result.points())) { + GeoPoint pt = RandomGeoGenerator.randomPoint(getRandom()); + result.point(pt.getLat(), pt.getLon()); + } + break; + case 1: + result.points(points(original.points())); + break; + case 2: + result.geoDistance(geoDistance(original.geoDistance())); + break; + case 3: + result.unit(unit(original.unit())); + break; + case 4: + result.order(RandomSortDataGenerator.order(original.order())); + break; + case 5: + result.sortMode(mode(original.sortMode())); + break; + case 6: + result.setNestedFilter(RandomSortDataGenerator.nestedFilter(original.getNestedFilter())); + break; + case 7: + result.setNestedPath(RandomSortDataGenerator.randomAscii(original.getNestedPath())); + break; + case 8: + result.coerce(! original.coerce()); + break; + case 9: + // ignore malformed will only be set if coerce is set to true + result.coerce(false); + result.ignoreMalformed(! original.ignoreMalformed()); + break; + } + return result; + + } + + @SuppressWarnings("unchecked") + @Override + protected Class getPrototype() { + return (Class) GeoDistanceSortBuilder.PROTOTYPE.getClass(); + } + + public void testSortModeSumIsRejectedInSetter() { + GeoDistanceSortBuilder builder = new GeoDistanceSortBuilder("testname", -1, -1); + GeoPoint point = RandomGeoGenerator.randomPoint(getRandom()); + builder.point(point.getLat(), point.getLon()); + try { + builder.sortMode("SUM"); + fail("sort mode sum should not be supported"); + } catch (IllegalArgumentException e) { + // all good + } + } + + public void testSortModeSumIsRejectedInJSON() throws IOException { + String json = "{\n" + + " \"testname\" : [ {\n" + + " \"lat\" : -6.046997540714173,\n" + + " \"lon\" : -51.94128329747579\n" + + " } ],\n" + + " \"unit\" : \"m\",\n" + + " \"distance_type\" : \"sloppy_arc\",\n" + + " \"reverse\" : true,\n" + + " \"mode\" : \"SUM\",\n" + + " \"coerce\" : false,\n" + + " \"ignore_malformed\" : false\n" + + "}"; + XContentParser itemParser = XContentHelper.createParser(new BytesArray(json)); + itemParser.nextToken(); + + QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); + context.reset(itemParser); + + try { + GeoDistanceSortBuilder.PROTOTYPE.fromXContent(context, ""); + fail("sort mode sum should not be supported"); + } catch (IllegalArgumentException e) { + // all good + } + } + + public void testGeoDistanceSortCanBeParsedFromGeoHash() throws IOException { + String json = "{\n" + + " \"VDcvDuFjE\" : [ \"7umzzv8eychg\", \"dmdgmt5z13uw\", \"ezu09wxw6v4c\", \"kc7s3515p6k6\", \"jgeuvjwrmfzn\", \"kcpcfj7ruyf8\" ],\n" + + " \"unit\" : \"m\",\n" + + " \"distance_type\" : \"sloppy_arc\",\n" + + " \"reverse\" : true,\n" + + " \"mode\" : \"MAX\",\n" + + " \"nested_filter\" : {\n" + + " \"ids\" : {\n" + + " \"type\" : [ ],\n" + + " \"values\" : [ ],\n" + + " \"boost\" : 5.711116\n" + + " }\n" + + " },\n" + + " \"coerce\" : false,\n" + + " \"ignore_malformed\" : true\n" + + " }"; + XContentParser itemParser = XContentHelper.createParser(new BytesArray(json)); + itemParser.nextToken(); + + QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); + context.reset(itemParser); + + GeoDistanceSortBuilder result = GeoDistanceSortBuilder.PROTOTYPE.fromXContent(context, json); + assertEquals("[-19.700583312660456, -2.8225036337971687, " + + "31.537466906011105, -74.63590376079082, " + + "43.71844606474042, -5.548660643398762, " + + "-37.20467280596495, 38.71751043945551, " + + "-69.44606635719538, 84.25200328230858, " + + "-39.03717711567879, 44.74099852144718]", Arrays.toString(result.points())); + } +} diff --git a/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java b/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java new file mode 100644 index 00000000000..fcd5284119c --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.sort; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.index.query.IdsQueryBuilder; +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.test.ESTestCase; + +public class RandomSortDataGenerator { + private RandomSortDataGenerator() { + // this is a helper class only, doesn't need a constructor + } + + public static QueryBuilder nestedFilter(QueryBuilder original) { + @SuppressWarnings("rawtypes") + QueryBuilder nested = null; + while (nested == null || nested.equals(original)) { + switch (ESTestCase.randomInt(2)) { + case 0: + nested = new MatchAllQueryBuilder(); + break; + case 1: + nested = new IdsQueryBuilder(); + break; + default: + case 2: + nested = new TermQueryBuilder(ESTestCase.randomAsciiOfLengthBetween(1, 10), ESTestCase.randomAsciiOfLengthBetween(1, 10)); + break; + } + nested.boost((float) ESTestCase.randomDoubleBetween(0, 10, false)); + } + return nested; + } + + public static String randomAscii(String original) { + String nestedPath = ESTestCase.randomAsciiOfLengthBetween(1, 10); + while (nestedPath.equals(original)) { + nestedPath = ESTestCase.randomAsciiOfLengthBetween(1, 10); + } + return nestedPath; + } + + public static String mode(String original) { + String[] modes = {"min", "max", "avg", "sum"}; + String mode = ESTestCase.randomFrom(modes); + while (mode.equals(original)) { + mode = ESTestCase.randomFrom(modes); + } + return mode; + } + + public static Object missing(Object original) { + Object missing = null; + Object otherMissing = null; + if (original instanceof BytesRef) { + otherMissing = ((BytesRef) original).utf8ToString(); + } else { + otherMissing = original; + } + + while (missing == null || missing.equals(otherMissing)) { + int missingId = ESTestCase.randomIntBetween(0, 3); + switch (missingId) { + case 0: + missing = ("_last"); + break; + case 1: + missing = ("_first"); + break; + case 2: + missing = ESTestCase.randomAsciiOfLength(10); + break; + case 3: + missing = ESTestCase.randomInt(); + break; + default: + throw new IllegalStateException("Unknown missing type."); + + } + } + return missing; + } + + public static SortOrder order(SortOrder original) { + SortOrder order = SortOrder.ASC; + if (order.equals(original)) { + return SortOrder.DESC; + } else { + return SortOrder.ASC; + } + } + +} diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java index f101303ee82..e0aafcfdb27 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java @@ -217,14 +217,14 @@ public class GeoDistanceTests extends ESIntegTestCase { // SORTING searchResponse = client().prepareSearch().setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("location").point(40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("location", 40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 7); assertOrderedSearchHits(searchResponse, "1", "3", "4", "5", "6", "2", "7"); searchResponse = client().prepareSearch().setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("location").point(40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("location", 40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 7); @@ -288,7 +288,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -301,7 +301,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc, Mode: max searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max")) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max")) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -314,7 +314,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -327,7 +327,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc, Mode: min searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min")) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min")) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -339,7 +339,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -351,7 +351,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(5301d, 10d)); searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -363,7 +363,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); assertFailures(client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("sum")), + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("sum")), RestStatus.BAD_REQUEST, containsString("sort_mode [sum] isn't supported for sorting by geo distance")); } @@ -399,7 +399,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 2); @@ -409,7 +409,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); // Doc with missing geo point is first, is consistent with 0.20.x @@ -578,7 +578,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.ASC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.ASC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -590,7 +590,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc, Mode: max searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max").setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max").setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -602,7 +602,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.DESC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.DESC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -614,7 +614,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc, Mode: min searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min").setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min").setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -625,7 +625,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(3).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -637,8 +637,8 @@ public class GeoDistanceTests extends ESIntegTestCase { searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) .addSort( - SortBuilders.geoDistanceSort("branches.location").setNestedPath("branches") - .point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC).setNestedPath("branches") + SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).setNestedPath("branches") + .sortMode("avg").order(SortOrder.DESC).setNestedPath("branches") ) .execute().actionGet(); @@ -651,8 +651,8 @@ public class GeoDistanceTests extends ESIntegTestCase { searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) .addSort( - SortBuilders.geoDistanceSort("branches.location").setNestedFilter(termQuery("branches.name", "brooklyn")) - .point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches") + SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).setNestedFilter(termQuery("branches.name", "brooklyn")) + .sortMode("avg").order(SortOrder.ASC).setNestedPath("branches") ) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -664,7 +664,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(3).sortValues()[0]).doubleValue(), equalTo(Double.MAX_VALUE)); assertFailures(client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).sortMode("sum").setNestedPath("branches")), + .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).sortMode("sum").setNestedPath("branches")), RestStatus.BAD_REQUEST, containsString("sort_mode [sum] isn't supported for sorting by geo distance")); } diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java index b0c77f54197..506cd75a57d 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java @@ -1759,7 +1759,7 @@ public class SimpleSortTests extends ESIntegTestCase { SearchResponse searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location", q).sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d1", "d2"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 2, 3, 2, DistanceUnit.KILOMETERS), 0.01d)); @@ -1767,7 +1767,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location", q).sortMode("min").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d2", "d1"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 1, 5, 1, DistanceUnit.KILOMETERS), 0.01d)); @@ -1775,7 +1775,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location", q).sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d1", "d2"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 2, 4, 1, DistanceUnit.KILOMETERS), 0.01d)); @@ -1783,7 +1783,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location", q).sortMode("max").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d2", "d1"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 1, 6, 2, DistanceUnit.KILOMETERS), 0.01d)); @@ -1835,13 +1835,22 @@ public class SimpleSortTests extends ESIntegTestCase { List qPoints = new ArrayList<>(); createQPoints(qHashes, qPoints); - GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); + + GeoDistanceSortBuilder geoDistanceSortBuilder = null; for (int i = 0; i < 4; i++) { int at = randomInt(3 - i); if (randomBoolean()) { - geoDistanceSortBuilder.geohashes(qHashes.get(at)); + if (geoDistanceSortBuilder == null) { + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); + } else { + geoDistanceSortBuilder.geohashes(qHashes.get(at)); + } } else { + if (geoDistanceSortBuilder == null) { + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); + } else { geoDistanceSortBuilder.points(qPoints.get(at)); + } } qHashes.remove(at); qPoints.remove(at); @@ -1874,8 +1883,7 @@ public class SimpleSortTests extends ESIntegTestCase { String hashPoint = "s037ms06g7h0"; - GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); - geoDistanceSortBuilder.geohashes(hashPoint); + GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", hashPoint); SearchResponse searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1883,8 +1891,7 @@ public class SimpleSortTests extends ESIntegTestCase { .execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); - geoDistanceSortBuilder.points(new GeoPoint(2, 2)); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", new GeoPoint(2, 2)); searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1892,8 +1899,7 @@ public class SimpleSortTests extends ESIntegTestCase { .execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); - geoDistanceSortBuilder.point(2, 2); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", 2, 2); searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1904,21 +1910,21 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").point(2.0, 2.0) + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", 2.0, 2.0) .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").geohashes("s037ms06g7h0") + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", "s037ms06g7h0") .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").point(2.0, 2.0) + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", 2.0, 2.0) .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); } From 2366b3593fa0e40e4b4006424e204da9051fe9c8 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Tue, 2 Feb 2016 11:54:00 +0100 Subject: [PATCH 19/49] BWC: Added 1.7 version constants and bwc indices --- .../main/java/org/elasticsearch/Version.java | 10 +++++++++- .../resources/indices/bwc/unsupported-1.7.4.zip | Bin 0 -> 89463 bytes .../resources/indices/bwc/unsupported-1.7.5.zip | Bin 0 -> 91886 bytes .../indices/bwc/unsupportedrepo-1.7.4.zip | Bin 0 -> 83566 bytes .../indices/bwc/unsupportedrepo-1.7.5.zip | Bin 0 -> 87341 bytes 5 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 core/src/test/resources/indices/bwc/unsupported-1.7.4.zip create mode 100644 core/src/test/resources/indices/bwc/unsupported-1.7.5.zip create mode 100644 core/src/test/resources/indices/bwc/unsupportedrepo-1.7.4.zip create mode 100644 core/src/test/resources/indices/bwc/unsupportedrepo-1.7.5.zip diff --git a/core/src/main/java/org/elasticsearch/Version.java b/core/src/main/java/org/elasticsearch/Version.java index e55800682dd..a07afcc9ea5 100644 --- a/core/src/main/java/org/elasticsearch/Version.java +++ b/core/src/main/java/org/elasticsearch/Version.java @@ -254,7 +254,11 @@ public class Version { public static final int V_1_7_3_ID = 1070399; public static final Version V_1_7_3 = new Version(V_1_7_3_ID, false, org.apache.lucene.util.Version.LUCENE_4_10_4); public static final int V_1_7_4_ID = 1070499; - public static final Version V_1_7_4 = new Version(V_1_7_4_ID, true, org.apache.lucene.util.Version.LUCENE_4_10_4); + public static final Version V_1_7_4 = new Version(V_1_7_4_ID, false, org.apache.lucene.util.Version.LUCENE_4_10_4); + public static final int V_1_7_5_ID = 1070599; + public static final Version V_1_7_5 = new Version(V_1_7_5_ID, false, org.apache.lucene.util.Version.LUCENE_4_10_4); + public static final int V_1_7_6_ID = 1070699; + public static final Version V_1_7_6 = new Version(V_1_7_6_ID, true, org.apache.lucene.util.Version.LUCENE_4_10_4); public static final int V_2_0_0_beta1_ID = 2000001; public static final Version V_2_0_0_beta1 = new Version(V_2_0_0_beta1_ID, false, org.apache.lucene.util.Version.LUCENE_5_2_1); @@ -321,6 +325,10 @@ public class Version { return V_2_0_0_beta2; case V_2_0_0_beta1_ID: return V_2_0_0_beta1; + case V_1_7_6_ID: + return V_1_7_6; + case V_1_7_5_ID: + return V_1_7_5; case V_1_7_4_ID: return V_1_7_4; case V_1_7_3_ID: diff --git a/core/src/test/resources/indices/bwc/unsupported-1.7.4.zip b/core/src/test/resources/indices/bwc/unsupported-1.7.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..a47ff4faffc93cecbd8f2a8b7e3e75ebb46698f0 GIT binary patch literal 89463 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

`8fcl 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 z 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 literal 0 HcmV?d00001 diff --git a/core/src/test/resources/indices/bwc/unsupportedrepo-1.7.4.zip b/core/src/test/resources/indices/bwc/unsupportedrepo-1.7.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..86be302153bd20559e796b954c3340abaaa2cfa6 GIT binary patch literal 83566 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∼^My&nCEHw$=d&)glx48>`#Ru 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�c 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 Tf`L(kenXJJz{Y=$kiq^RPwxxr literal 0 HcmV?d00001 diff --git a/core/src/test/resources/indices/bwc/unsupportedrepo-1.7.5.zip b/core/src/test/resources/indices/bwc/unsupportedrepo-1.7.5.zip new file mode 100644 index 0000000000000000000000000000000000000000..46bada25ced850b67196ebb75ab9d513fb912152 GIT binary patch literal 87341 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 z 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 Date: Mon, 1 Feb 2016 22:00:51 +0100 Subject: [PATCH 20/49] Remove `discovery.zen.rejoin_on_master_gone` That setting was built as an escape hatch for the work done in 1.4. I never heard it being used once. We can safely remove it. Closes #16353 --- .../common/settings/ClusterSettings.java | 2 - .../discovery/zen/ZenDiscovery.java | 49 ++----------------- .../discovery/zen/ZenDiscoveryIT.java | 14 ------ 3 files changed, 4 insertions(+), 61 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index ea16c6aabd6..616a7328ecc 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -58,7 +58,6 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.gateway.PrimaryShardAllocator; import org.elasticsearch.http.HttpTransportSettings; -import org.elasticsearch.http.netty.NettyHttpServerTransport; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.store.IndexStoreConfig; import org.elasticsearch.indices.analysis.HunspellService; @@ -160,7 +159,6 @@ public final class ClusterSettings extends AbstractScopedSettings { ConcurrentRebalanceAllocationDecider.CLUSTER_ROUTING_ALLOCATION_CLUSTER_CONCURRENT_REBALANCE_SETTING, EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING, EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING, - ZenDiscovery.REJOIN_ON_MASTER_GONE_SETTING, FilterAllocationDecider.CLUSTER_ROUTING_INCLUDE_GROUP_SETTING, FilterAllocationDecider.CLUSTER_ROUTING_EXCLUDE_GROUP_SETTING, FilterAllocationDecider.CLUSTER_ROUTING_REQUIRE_GROUP_SETTING, diff --git a/core/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java b/core/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java index 55eaf78b7a2..ffa29c857ea 100644 --- a/core/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java +++ b/core/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java @@ -89,17 +89,16 @@ import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; */ public class ZenDiscovery extends AbstractLifecycleComponent implements Discovery, PingContextProvider { - public final static Setting REJOIN_ON_MASTER_GONE_SETTING = Setting.boolSetting("discovery.zen.rejoin_on_master_gone", true, true, Setting.Scope.CLUSTER); public final static Setting PING_TIMEOUT_SETTING = Setting.positiveTimeSetting("discovery.zen.ping_timeout", timeValueSeconds(3), false, Setting.Scope.CLUSTER); public final static Setting JOIN_TIMEOUT_SETTING = Setting.timeSetting("discovery.zen.join_timeout", - settings -> TimeValue.timeValueMillis(PING_TIMEOUT_SETTING.get(settings).millis() * 20).toString(), TimeValue.timeValueMillis(0), false, Setting.Scope.CLUSTER); + settings -> TimeValue.timeValueMillis(PING_TIMEOUT_SETTING.get(settings).millis() * 20).toString(), TimeValue.timeValueMillis(0), false, Setting.Scope.CLUSTER); public final static Setting JOIN_RETRY_ATTEMPTS_SETTING = Setting.intSetting("discovery.zen.join_retry_attempts", 3, 1, false, Setting.Scope.CLUSTER); public final static Setting JOIN_RETRY_DELAY_SETTING = Setting.positiveTimeSetting("discovery.zen.join_retry_delay", TimeValue.timeValueMillis(100), false, Setting.Scope.CLUSTER); public final static Setting MAX_PINGS_FROM_ANOTHER_MASTER_SETTING = Setting.intSetting("discovery.zen.max_pings_from_another_master", 3, 1, false, Setting.Scope.CLUSTER); public final static Setting SEND_LEAVE_REQUEST_SETTING = Setting.boolSetting("discovery.zen.send_leave_request", true, false, Setting.Scope.CLUSTER); public final static Setting MASTER_ELECTION_FILTER_CLIENT_SETTING = Setting.boolSetting("discovery.zen.master_election.filter_client", true, false, Setting.Scope.CLUSTER); public final static Setting MASTER_ELECTION_WAIT_FOR_JOINS_TIMEOUT_SETTING = Setting.timeSetting("discovery.zen.master_election.wait_for_joins_timeout", - settings -> TimeValue.timeValueMillis(JOIN_TIMEOUT_SETTING.get(settings).millis() / 2).toString(), TimeValue.timeValueMillis(0), false, Setting.Scope.CLUSTER); + settings -> TimeValue.timeValueMillis(JOIN_TIMEOUT_SETTING.get(settings).millis() / 2).toString(), TimeValue.timeValueMillis(0), false, Setting.Scope.CLUSTER); public final static Setting MASTER_ELECTION_FILTER_DATA_SETTING = Setting.boolSetting("discovery.zen.master_election.filter_data", false, false, Setting.Scope.CLUSTER); public static final String DISCOVERY_REJOIN_ACTION_NAME = "internal:discovery/zen/rejoin"; @@ -142,8 +141,6 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen private final AtomicBoolean initialStateSent = new AtomicBoolean(); - private volatile boolean rejoinOnMasterGone; - /** counts the time this node has joined the cluster or have elected it self as master */ private final AtomicLong clusterJoinsCounter = new AtomicLong(); @@ -177,7 +174,6 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen this.masterElectionFilterClientNodes = MASTER_ELECTION_FILTER_CLIENT_SETTING.get(settings); this.masterElectionFilterDataNodes = MASTER_ELECTION_FILTER_DATA_SETTING.get(settings); this.masterElectionWaitForJoinsTimeout = MASTER_ELECTION_WAIT_FOR_JOINS_TIMEOUT_SETTING.get(settings); - this.rejoinOnMasterGone = REJOIN_ON_MASTER_GONE_SETTING.get(settings); logger.debug("using ping_timeout [{}], join.timeout [{}], master_election.filter_client [{}], master_election.filter_data [{}]", this.pingTimeout, joinTimeout, masterElectionFilterClientNodes, masterElectionFilterDataNodes); @@ -188,7 +184,6 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen throw new IllegalArgumentException("cannot set " + ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING.getKey() + " to more than the current master nodes count [" + masterNodes + "]"); } }); - clusterSettings.addSettingsUpdateConsumer(REJOIN_ON_MASTER_GONE_SETTING, this::setRejoingOnMasterGone); this.masterFD = new MasterFaultDetection(settings, threadPool, transportService, clusterName, clusterService); this.masterFD.addListener(new MasterNodeFailureListener()); @@ -323,10 +318,6 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen return clusterJoinsCounter.get() > 0; } - private void setRejoingOnMasterGone(boolean rejoin) { - this.rejoinOnMasterGone = rejoin; - } - /** end of {@link org.elasticsearch.discovery.zen.ping.PingContextProvider } implementation */ @@ -670,35 +661,7 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen // flush any pending cluster states from old master, so it will not be set as master again publishClusterState.pendingStatesQueue().failAllStatesAndClear(new ElasticsearchException("master left [{}]", reason)); - if (rejoinOnMasterGone) { - return rejoin(ClusterState.builder(currentState).nodes(discoveryNodes).build(), "master left (reason = " + reason + ")"); - } - - if (!electMaster.hasEnoughMasterNodes(discoveryNodes)) { - return rejoin(ClusterState.builder(currentState).nodes(discoveryNodes).build(), "not enough master nodes after master left (reason = " + reason + ")"); - } - - final DiscoveryNode electedMaster = electMaster.electMaster(discoveryNodes); // elect master - final DiscoveryNode localNode = currentState.nodes().localNode(); - if (localNode.equals(electedMaster)) { - masterFD.stop("got elected as new master since master left (reason = " + reason + ")"); - discoveryNodes = DiscoveryNodes.builder(discoveryNodes).masterNodeId(localNode.id()).build(); - ClusterState newState = ClusterState.builder(currentState).nodes(discoveryNodes).build(); - nodesFD.updateNodesAndPing(newState); - return newState; - - } else { - nodesFD.stop(); - if (electedMaster != null) { - discoveryNodes = DiscoveryNodes.builder(discoveryNodes).masterNodeId(electedMaster.id()).build(); - masterFD.restart(electedMaster, "possible elected master since master left (reason = " + reason + ")"); - return ClusterState.builder(currentState) - .nodes(discoveryNodes) - .build(); - } else { - return rejoin(ClusterState.builder(currentState).nodes(discoveryNodes).build(), "master_left and no other node elected to become master"); - } - } + return rejoin(ClusterState.builder(currentState).nodes(discoveryNodes).build(), "master left (reason = " + reason + ")"); } @Override @@ -857,7 +820,7 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen // Sanity check: maybe we don't end up here, because serialization may have failed. if (node.getVersion().before(minimumNodeJoinVersion)) { callback.onFailure( - new IllegalStateException("Can't handle join request from a node with a version [" + node.getVersion() + "] that is lower than the minimum compatible version [" + minimumNodeJoinVersion.minimumCompatibilityVersion() + "]") + new IllegalStateException("Can't handle join request from a node with a version [" + node.getVersion() + "] that is lower than the minimum compatible version [" + minimumNodeJoinVersion.minimumCompatibilityVersion() + "]") ); return; } @@ -1109,10 +1072,6 @@ public class ZenDiscovery extends AbstractLifecycleComponent implemen } } - boolean isRejoinOnMasterGone() { - return rejoinOnMasterGone; - } - public static class RejoinClusterRequest extends TransportRequest { private String fromNodeId; diff --git a/core/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryIT.java b/core/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryIT.java index 6c564a97740..ee92945c4ff 100644 --- a/core/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryIT.java +++ b/core/src/test/java/org/elasticsearch/discovery/zen/ZenDiscoveryIT.java @@ -80,20 +80,6 @@ import static org.hamcrest.Matchers.sameInstance; @ESIntegTestCase.SuppressLocalMode @TestLogging("_root:DEBUG") public class ZenDiscoveryIT extends ESIntegTestCase { - public void testChangeRejoinOnMasterOptionIsDynamic() throws Exception { - Settings nodeSettings = Settings.settingsBuilder() - .put("discovery.type", "zen") // <-- To override the local setting if set externally - .build(); - String nodeName = internalCluster().startNode(nodeSettings); - ZenDiscovery zenDiscovery = (ZenDiscovery) internalCluster().getInstance(Discovery.class, nodeName); - assertThat(zenDiscovery.isRejoinOnMasterGone(), is(true)); - - client().admin().cluster().prepareUpdateSettings() - .setTransientSettings(Settings.builder().put(ZenDiscovery.REJOIN_ON_MASTER_GONE_SETTING.getKey(), false)) - .get(); - - assertThat(zenDiscovery.isRejoinOnMasterGone(), is(false)); - } public void testNoShardRelocationsOccurWhenElectedMasterNodeFails() throws Exception { Settings defaultSettings = Settings.builder() From 51862ce5aec97ba1e7bfd8f765129ce395a976b6 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 2 Feb 2016 12:57:19 +0100 Subject: [PATCH 21/49] Fix IndexShardTests.testStressRelocated Closes #16364 --- .../org/elasticsearch/index/shard/IndexShardTests.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index bedda8d3c61..a77e75d1356 100644 --- a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -816,7 +816,6 @@ public class IndexShardTests extends ESSingleNodeTestCase { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/16364") public void testStressRelocated() throws Exception { assertAcked(client().admin().indices().prepareCreate("test").setSettings( Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0) @@ -827,14 +826,14 @@ public class IndexShardTests extends ESSingleNodeTestCase { final IndexShard shard = test.getShardOrNull(0); final int numThreads = randomIntBetween(2, 4); Thread[] indexThreads = new Thread[numThreads]; - CountDownLatch somePrimaryOperationLockAcquired = new CountDownLatch(1); + CountDownLatch allPrimaryOperationLocksAcquired = new CountDownLatch(numThreads); CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); for (int i = 0; i < indexThreads.length; i++) { indexThreads[i] = new Thread() { @Override public void run() { try (Releasable operationLock = shard.acquirePrimaryOperationLock()) { - somePrimaryOperationLockAcquired.countDown(); + allPrimaryOperationLocksAcquired.countDown(); barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { throw new RuntimeException(e); @@ -848,8 +847,8 @@ public class IndexShardTests extends ESSingleNodeTestCase { shard.relocated("simulated recovery"); relocated.set(true); }); - // ensure we wait for at least one primary operation lock to be acquired - somePrimaryOperationLockAcquired.await(); + // ensure we wait for all primary operation locks to be acquired + allPrimaryOperationLocksAcquired.await(); // start recovery thread recoveryThread.start(); assertThat(relocated.get(), equalTo(false)); From b5a4e99344f2c76d7edd06508f02b7d1a67c2be5 Mon Sep 17 00:00:00 2001 From: Isabel Drost-Fromm Date: Tue, 2 Feb 2016 13:09:34 +0100 Subject: [PATCH 22/49] Remove deprecation for geohash setter This removes the deprecation for the geohash based setter to quickly fix the failure here: http://build-us-00.elastic.co/job/es_core_master_suse/3312 Reintroducing postponed until related test in groovy module is fixed. Need to figure out what went wrong when I ran the build locally w/o failure before. --- .../org/elasticsearch/search/sort/GeoDistanceSortBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index 428f81dba73..204779299ba 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -165,7 +165,6 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N * * Deprecated - please use points(GeoPoint... points) instead. */ - @Deprecated public GeoDistanceSortBuilder geohashes(String... geohashes) { for (String geohash : geohashes) { this.points.add(GeoPoint.fromGeohash(geohash)); From 26f77eb70d68a9382d64535c8dbd07d42842d252 Mon Sep 17 00:00:00 2001 From: Isabel Drost-Fromm Date: Tue, 2 Feb 2016 13:46:25 +0100 Subject: [PATCH 23/49] Indentation fix for messy SimpleSortTest --- .../elasticsearch/messy/tests/SimpleSortTests.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java index 506cd75a57d..2c167cd34eb 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java @@ -1840,14 +1840,14 @@ public class SimpleSortTests extends ESIntegTestCase { for (int i = 0; i < 4; i++) { int at = randomInt(3 - i); if (randomBoolean()) { - if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); - } else { - geoDistanceSortBuilder.geohashes(qHashes.get(at)); - } + if (geoDistanceSortBuilder == null) { + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); + } else { + geoDistanceSortBuilder.geohashes(qHashes.get(at)); + } } else { if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); } else { geoDistanceSortBuilder.points(qPoints.get(at)); } From 10b3071e167e5837e509de16121e90cb38749293 Mon Sep 17 00:00:00 2001 From: Prayag Verma Date: Tue, 2 Feb 2016 18:16:58 +0530 Subject: [PATCH 24/49] Fix minor typo in migrate_3_0.asciidoc Remove extra `on` --- docs/reference/migration/migrate_3_0.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/migration/migrate_3_0.asciidoc b/docs/reference/migration/migrate_3_0.asciidoc index 1c386b2ecfa..0ea68ecacc8 100644 --- a/docs/reference/migration/migrate_3_0.asciidoc +++ b/docs/reference/migration/migrate_3_0.asciidoc @@ -166,7 +166,7 @@ with `_parent` field mapping created before version `2.0.0`. The data of these i The format of the join between parent and child documents have changed with the `2.0.0` release. The old format can't read from version `3.0.0` and onwards. The new format allows for a much more efficient and -scalable join between parent and child documents and the join data structures are stored on on disk +scalable join between parent and child documents and the join data structures are stored on disk data structures as opposed as before the join data structures were stored in the jvm heap space. ==== `score_type` has been removed From af1f6375478658fd1950184be0199128080e8a91 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Wed, 20 Jan 2016 16:19:42 +0100 Subject: [PATCH 25/49] Prevent TransportReplicationAction to route request based on stale local routing table Closes #16274 Closes #12573 Closes #12574 --- .../replication/ReplicationRequest.java | 18 +++++++ .../TransportReplicationAction.java | 9 ++++ .../TransportReplicationActionTests.java | 54 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/core/src/main/java/org/elasticsearch/action/support/replication/ReplicationRequest.java b/core/src/main/java/org/elasticsearch/action/support/replication/ReplicationRequest.java index ed23017410e..4e6ec3c3584 100644 --- a/core/src/main/java/org/elasticsearch/action/support/replication/ReplicationRequest.java +++ b/core/src/main/java/org/elasticsearch/action/support/replication/ReplicationRequest.java @@ -55,6 +55,8 @@ public abstract class ReplicationRequest relocation ongoing state:\n{}", clusterService.state().prettyPrint()); + + Request request = new Request(shardId).timeout("1ms").routedBasedOnClusterVersion(clusterService.state().version() + 1); + PlainActionFuture listener = new PlainActionFuture<>(); + TransportReplicationAction.ReroutePhase reroutePhase = action.new ReroutePhase(request, listener); + reroutePhase.run(); + assertListenerThrows("cluster state too old didn't cause a timeout", listener, UnavailableShardsException.class); + + request = new Request(shardId).routedBasedOnClusterVersion(clusterService.state().version() + 1); + listener = new PlainActionFuture<>(); + reroutePhase = action.new ReroutePhase(request, listener); + reroutePhase.run(); + assertFalse("cluster state too old didn't cause a retry", listener.isDone()); + + // finish relocation + ShardRouting relocationTarget = clusterService.state().getRoutingTable().shardRoutingTable(shardId).shardsWithState(ShardRoutingState.INITIALIZING).get(0); + AllocationService allocationService = ESAllocationTestCase.createAllocationService(); + RoutingAllocation.Result result = allocationService.applyStartedShards(state, Arrays.asList(relocationTarget)); + ClusterState updatedState = ClusterState.builder(clusterService.state()).routingResult(result).build(); + + clusterService.setState(updatedState); + logger.debug("--> relocation complete state:\n{}", clusterService.state().prettyPrint()); + + IndexShardRoutingTable shardRoutingTable = clusterService.state().routingTable().index(index).shard(shardId.id()); + final String primaryNodeId = shardRoutingTable.primaryShard().currentNodeId(); + final List capturedRequests = + transport.getCapturedRequestsByTargetNodeAndClear().get(primaryNodeId); + assertThat(capturedRequests, notNullValue()); + assertThat(capturedRequests.size(), equalTo(1)); + assertThat(capturedRequests.get(0).action, equalTo("testAction[p]")); + assertIndexShardCounter(1); + } + public void testUnknownIndexOrShardOnReroute() throws InterruptedException { final String index = "test"; // no replicas in oder to skip the replication part From cd537721783733813d23bf7b3b1ab69172a6c947 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Mon, 1 Feb 2016 17:12:20 +0100 Subject: [PATCH 26/49] Use allocation ids to prevent repeated recovery of failed shards Closes #16346 --- .../cluster/IndicesClusterStateService.java | 70 +++++-------------- 1 file changed, 19 insertions(+), 51 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index 7fc12eb8bab..f5afec1d5e3 100644 --- a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -39,13 +39,11 @@ import org.elasticsearch.cluster.routing.RoutingNodes; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.Callback; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.index.IndexService; @@ -93,26 +91,12 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent, Boolean> seenMappings = ConcurrentCollections.newConcurrentMap(); - // a list of shards that failed during recovery // we keep track of these shards in order to prevent repeated recovery of these shards on each cluster state update - private final ConcurrentMap failedShards = ConcurrentCollections.newConcurrentMap(); + private final ConcurrentMap failedShards = ConcurrentCollections.newConcurrentMap(); private final RestoreService restoreService; private final RepositoriesService repositoriesService; - static class FailedShard { - public final long version; - public final long timestamp; - - FailedShard(long version) { - this.version = version; - this.timestamp = System.currentTimeMillis(); - } - } - private final Object mutex = new Object(); private final FailedShardHandler failedShardHandler = new FailedShardHandler(); @@ -431,11 +415,6 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent> iterator = failedShards.entrySet().iterator(); - shards: - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - FailedShard failedShard = entry.getValue(); - IndexRoutingTable indexRoutingTable = routingTable.index(entry.getKey().getIndex()); - if (indexRoutingTable != null) { - IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(entry.getKey().id()); - if (shardRoutingTable != null) { - for (ShardRouting shardRouting : shardRoutingTable.assignedShards()) { - if (localNodeId.equals(shardRouting.currentNodeId())) { - // we have a timeout here just to make sure we don't have dangled failed shards for some reason - // its just another safely layer - if (shardRouting.version() == failedShard.version && ((now - failedShard.timestamp) < TimeValue.timeValueMinutes(60).millis())) { - // It's the same failed shard - keep it if it hasn't timed out - continue shards; - } else { - // Different version or expired, remove it - break; - } - } - } - } + RoutingTable routingTable = event.state().routingTable(); + for (Iterator> iterator = failedShards.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + ShardId failedShardId = entry.getKey(); + ShardRouting failedShardRouting = entry.getValue(); + IndexRoutingTable indexRoutingTable = routingTable.index(failedShardId.getIndex()); + if (indexRoutingTable == null) { + iterator.remove(); + continue; + } + IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(failedShardId.id()); + if (shardRoutingTable == null) { + iterator.remove(); + continue; + } + if (shardRoutingTable.assignedShards().stream().noneMatch(shr -> shr.isSameAllocation(failedShardRouting))) { + iterator.remove(); } - iterator.remove(); } } @@ -788,7 +756,7 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent Date: Tue, 2 Feb 2016 14:03:40 +0100 Subject: [PATCH 27/49] Revert "Indentation fix for messy SimpleSortTest" This reverts commit 26f77eb70d68a9382d64535c8dbd07d42842d252. --- .../elasticsearch/messy/tests/SimpleSortTests.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java index 2c167cd34eb..506cd75a57d 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java @@ -1840,14 +1840,14 @@ public class SimpleSortTests extends ESIntegTestCase { for (int i = 0; i < 4; i++) { int at = randomInt(3 - i); if (randomBoolean()) { - if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); - } else { - geoDistanceSortBuilder.geohashes(qHashes.get(at)); - } + if (geoDistanceSortBuilder == null) { + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); + } else { + geoDistanceSortBuilder.geohashes(qHashes.get(at)); + } } else { if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); } else { geoDistanceSortBuilder.points(qPoints.get(at)); } From 089ab7d9bf769e78c279f463c103944340ab16dc Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 2 Feb 2016 14:03:58 +0100 Subject: [PATCH 28/49] Revert "Remove deprecation for geohash setter" This reverts commit b5a4e99344f2c76d7edd06508f02b7d1a67c2be5. --- .../org/elasticsearch/search/sort/GeoDistanceSortBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index 204779299ba..428f81dba73 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -165,6 +165,7 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N * * Deprecated - please use points(GeoPoint... points) instead. */ + @Deprecated public GeoDistanceSortBuilder geohashes(String... geohashes) { for (String geohash : geohashes) { this.points.add(GeoPoint.fromGeohash(geohash)); From 8cca0395ef6449651def09bf1e96465bad2beaa4 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 2 Feb 2016 14:04:45 +0100 Subject: [PATCH 29/49] Revert "Make GeoDistanceSortBuilder serializable" This reverts commit c108a4ce6d32f43397d5adbcb4b930f5be174d48. --- .../search/sort/GeoDistanceSortBuilder.java | 384 ++---------------- .../search/sort/GeoDistanceSortParser.java | 28 +- .../search/sort/SortBuilders.java | 30 +- .../search/sort/SortElementParserTemp.java | 40 -- .../elasticsearch/search/sort/SortOrder.java | 3 +- .../builder/SearchSourceBuilderTests.java | 4 +- .../search/sort/AbstractSortTestCase.java | 162 -------- .../sort/GeoDistanceSortBuilderTests.java | 251 ------------ .../search/sort/RandomSortDataGenerator.java | 113 ------ .../messy/tests/GeoDistanceTests.java | 42 +- .../messy/tests/SimpleSortTests.java | 36 +- 11 files changed, 100 insertions(+), 993 deletions(-) delete mode 100644 core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java delete mode 100644 core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java delete mode 100644 core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java delete mode 100644 core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index 428f81dba73..8f502e53058 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -22,114 +22,41 @@ package org.elasticsearch.search.sort; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.geo.GeoUtils; -import org.elasticsearch.common.io.stream.NamedWriteable; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.DistanceUnit; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryParseContext; -import org.elasticsearch.search.MultiValueMode; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.Objects; /** * A geo distance based sorting on a geo point like field. */ -public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, NamedWriteable, SortElementParserTemp { - public static final String NAME = "_geo_distance"; - public static final boolean DEFAULT_COERCE = false; - public static final boolean DEFAULT_IGNORE_MALFORMED = false; +public class GeoDistanceSortBuilder extends SortBuilder { - static final GeoDistanceSortBuilder PROTOTYPE = new GeoDistanceSortBuilder("", -1, -1); - - private final String fieldName; + final String fieldName; private final List points = new ArrayList<>(); + private final List geohashes = new ArrayList<>(); - private GeoDistance geoDistance = GeoDistance.DEFAULT; - private DistanceUnit unit = DistanceUnit.DEFAULT; - private SortOrder order = SortOrder.ASC; - - // TODO there is an enum that covers that parameter which we should be using here - private String sortMode = null; - @SuppressWarnings("rawtypes") + private GeoDistance geoDistance; + private DistanceUnit unit; + private SortOrder order; + private String sortMode; private QueryBuilder nestedFilter; private String nestedPath; - - // TODO switch to GeoValidationMethod enum - private boolean coerce = DEFAULT_COERCE; - private boolean ignoreMalformed = DEFAULT_IGNORE_MALFORMED; + private Boolean coerce; + private Boolean ignoreMalformed; /** * Constructs a new distance based sort on a geo point like field. * * @param fieldName The geo point like field name. - * @param points The points to create the range distance facets from. */ - public GeoDistanceSortBuilder(String fieldName, GeoPoint... points) { + public GeoDistanceSortBuilder(String fieldName) { this.fieldName = fieldName; - if (points.length == 0) { - throw new IllegalArgumentException("Geo distance sorting needs at least one point."); - } - this.points.addAll(Arrays.asList(points)); - } - - /** - * Constructs a new distance based sort on a geo point like field. - * - * @param fieldName The geo point like field name. - * @param lat Latitude of the point to create the range distance facets from. - * @param lon Longitude of the point to create the range distance facets from. - */ - public GeoDistanceSortBuilder(String fieldName, double lat, double lon) { - this(fieldName, new GeoPoint(lat, lon)); - } - - /** - * Constructs a new distance based sort on a geo point like field. - * - * @param fieldName The geo point like field name. - * @param geohashes The points to create the range distance facets from. - */ - public GeoDistanceSortBuilder(String fieldName, String ... geohashes) { - if (geohashes.length == 0) { - throw new IllegalArgumentException("Geo distance sorting needs at least one point."); - } - for (String geohash : geohashes) { - this.points.add(GeoPoint.fromGeohash(geohash)); - } - this.fieldName = fieldName; - } - - /** - * Copy constructor. - * */ - GeoDistanceSortBuilder(GeoDistanceSortBuilder original) { - this.fieldName = original.fieldName(); - this.points.addAll(original.points); - this.geoDistance = original.geoDistance; - this.unit = original.unit; - this.order = original.order; - this.sortMode = original.sortMode; - this.nestedFilter = original.nestedFilter; - this.nestedPath = original.nestedPath; - this.coerce = original.coerce; - this.ignoreMalformed = original.ignoreMalformed; - } - - /** - * Returns the geo point like field the distance based sort operates on. - * */ - public String fieldName() { - return this.fieldName; } /** @@ -152,27 +79,15 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N this.points.addAll(Arrays.asList(points)); return this; } - - /** - * Returns the points to create the range distance facets from. - */ - public GeoPoint[] points() { - return this.points.toArray(new GeoPoint[this.points.size()]); - } /** * The geohash of the geo point to create the range distance facets from. - * - * Deprecated - please use points(GeoPoint... points) instead. */ - @Deprecated public GeoDistanceSortBuilder geohashes(String... geohashes) { - for (String geohash : geohashes) { - this.points.add(GeoPoint.fromGeohash(geohash)); - } + this.geohashes.addAll(Arrays.asList(geohashes)); return this; } - + /** * The geo distance type used to compute the distance. */ @@ -180,13 +95,6 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N this.geoDistance = geoDistance; return this; } - - /** - * Returns the geo distance type used to compute the distance. - */ - public GeoDistance geoDistance() { - return this.geoDistance; - } /** * The distance unit to use. Defaults to {@link org.elasticsearch.common.unit.DistanceUnit#KILOMETERS} @@ -196,13 +104,6 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N return this; } - /** - * Returns the distance unit to use. Defaults to {@link org.elasticsearch.common.unit.DistanceUnit#KILOMETERS} - */ - public DistanceUnit unit() { - return this.unit; - } - /** * The order of sorting. Defaults to {@link SortOrder#ASC}. */ @@ -212,18 +113,11 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N return this; } - /** Returns the order of sorting. */ - public SortOrder order() { - return this.order; - } - /** * Not relevant. - * - * TODO should this throw an exception rather than silently ignore a parameter that is not used? */ @Override - public GeoDistanceSortBuilder missing(Object missing) { + public SortBuilder missing(Object missing) { return this; } @@ -232,19 +126,10 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N * Possible values: min and max */ public GeoDistanceSortBuilder sortMode(String sortMode) { - MultiValueMode temp = MultiValueMode.fromString(sortMode); - if (temp == MultiValueMode.SUM) { - throw new IllegalArgumentException("sort_mode [sum] isn't supported for sorting by geo distance"); - } this.sortMode = sortMode; return this; } - /** Returns which distance to use for sorting in the case a document contains multiple geo points. */ - public String sortMode() { - return this.sortMode; - } - /** * Sets the nested filter that the nested objects should match with in order to be taken into account * for sorting. @@ -254,14 +139,6 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N return this; } - /** - * Returns the nested filter that the nested objects should match with in order to be taken into account - * for sorting. - **/ - public QueryBuilder getNestedFilter() { - return this.nestedFilter; - } - /** * Sets the nested path if sorting occurs on a field that is inside a nested object. By default when sorting on a * field inside a nested object, the nearest upper nested object is selected as nested path. @@ -270,53 +147,42 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N this.nestedPath = nestedPath; return this; } - - /** - * Returns the nested path if sorting occurs on a field that is inside a nested object. By default when sorting on a - * field inside a nested object, the nearest upper nested object is selected as nested path. - */ - public String getNestedPath() { - return this.nestedPath; - } public GeoDistanceSortBuilder coerce(boolean coerce) { this.coerce = coerce; return this; } - public boolean coerce() { - return this.coerce; - } - public GeoDistanceSortBuilder ignoreMalformed(boolean ignoreMalformed) { - if (coerce == false) { - this.ignoreMalformed = ignoreMalformed; - } + this.ignoreMalformed = ignoreMalformed; return this; } - - public boolean ignoreMalformed() { - return this.ignoreMalformed; - } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(NAME); + builder.startObject("_geo_distance"); + if (geohashes.size() == 0 && points.size() == 0) { + throw new ElasticsearchParseException("No points provided for _geo_distance sort."); + } builder.startArray(fieldName); for (GeoPoint point : points) { builder.value(point); } + for (String geohash : geohashes) { + builder.value(geohash); + } builder.endArray(); - builder.field("unit", unit); - builder.field("distance_type", geoDistance.name().toLowerCase(Locale.ROOT)); + if (unit != null) { + builder.field("unit", unit); + } + if (geoDistance != null) { + builder.field("distance_type", geoDistance.name().toLowerCase(Locale.ROOT)); + } if (order == SortOrder.DESC) { builder.field("reverse", true); - } else { - builder.field("reverse", false); } - if (sortMode != null) { builder.field("mode", sortMode); } @@ -327,198 +193,14 @@ public class GeoDistanceSortBuilder extends SortBuilder implements ToXContent, N if (nestedFilter != null) { builder.field("nested_filter", nestedFilter, params); } - builder.field("coerce", coerce); - builder.field("ignore_malformed", ignoreMalformed); + if (coerce != null) { + builder.field("coerce", coerce); + } + if (ignoreMalformed != null) { + builder.field("ignore_malformed", ignoreMalformed); + } builder.endObject(); return builder; } - - @Override - public String getWriteableName() { - return NAME; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - - if (object == null || getClass() != object.getClass()) { - return false; - } - - GeoDistanceSortBuilder other = (GeoDistanceSortBuilder) object; - return Objects.equals(fieldName, other.fieldName) && - Objects.deepEquals(points, other.points) && - Objects.equals(geoDistance, other.geoDistance) && - Objects.equals(unit, other.unit) && - Objects.equals(sortMode, other.sortMode) && - Objects.equals(order, other.order) && - Objects.equals(nestedFilter, other.nestedFilter) && - Objects.equals(nestedPath, other.nestedPath) && - Objects.equals(coerce, other.coerce) && - Objects.equals(ignoreMalformed, other.ignoreMalformed); - } - - @Override - public int hashCode() { - return Objects.hash(this.fieldName, this.points, this.geoDistance, - this.unit, this.sortMode, this.order, this.nestedFilter, this.nestedPath, this.coerce, this.ignoreMalformed); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(fieldName); - out.writeGenericValue(points); - - geoDistance.writeTo(out); - unit.writeTo(out); - order.writeTo(out); - out.writeOptionalString(sortMode); - if (nestedFilter != null) { - out.writeBoolean(true); - out.writeQuery(nestedFilter); - } else { - out.writeBoolean(false); - } - out.writeOptionalString(nestedPath); - out.writeBoolean(coerce); - out.writeBoolean(ignoreMalformed); - } - - @Override - public GeoDistanceSortBuilder readFrom(StreamInput in) throws IOException { - String fieldName = in.readString(); - - ArrayList points = (ArrayList) in.readGenericValue(); - GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(fieldName, points.toArray(new GeoPoint[points.size()])); - - result.geoDistance(GeoDistance.readGeoDistanceFrom(in)); - result.unit(DistanceUnit.readDistanceUnit(in)); - result.order(SortOrder.readOrderFrom(in)); - String sortMode = in.readOptionalString(); - if (sortMode != null) { - result.sortMode(sortMode); - } - if (in.readBoolean()) { - result.setNestedFilter(in.readQuery()); - } - result.setNestedPath(in.readOptionalString()); - result.coerce(in.readBoolean()); - result.ignoreMalformed(in.readBoolean()); - return result; - } - - @Override - public GeoDistanceSortBuilder fromXContent(QueryParseContext context, String elementName) throws IOException { - XContentParser parser = context.parser(); - String fieldName = null; - List geoPoints = new ArrayList<>(); - DistanceUnit unit = DistanceUnit.DEFAULT; - GeoDistance geoDistance = GeoDistance.DEFAULT; - boolean reverse = false; - MultiValueMode sortMode = null; - QueryBuilder nestedFilter = null; - String nestedPath = null; - - boolean coerce = GeoDistanceSortBuilder.DEFAULT_COERCE; - boolean ignoreMalformed = GeoDistanceSortBuilder.DEFAULT_IGNORE_MALFORMED; - - XContentParser.Token token; - String currentName = parser.currentName(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentName = parser.currentName(); - } else if (token == XContentParser.Token.START_ARRAY) { - parseGeoPoints(parser, geoPoints); - - fieldName = currentName; - } else if (token == XContentParser.Token.START_OBJECT) { - // the json in the format of -> field : { lat : 30, lon : 12 } - if ("nested_filter".equals(currentName) || "nestedFilter".equals(currentName)) { - // TODO Note to remember: while this is kept as a QueryBuilder internally, - // we need to make sure to call toFilter() on it once on the shard - // (e.g. in the new build() method) - nestedFilter = context.parseInnerQueryBuilder(); - } else { - fieldName = currentName; - GeoPoint point = new GeoPoint(); - GeoUtils.parseGeoPoint(parser, point); - geoPoints.add(point); - } - } else if (token.isValue()) { - if ("reverse".equals(currentName)) { - reverse = parser.booleanValue(); - } else if ("order".equals(currentName)) { - reverse = "desc".equals(parser.text()); - } else if ("unit".equals(currentName)) { - unit = DistanceUnit.fromString(parser.text()); - } else if ("distance_type".equals(currentName) || "distanceType".equals(currentName)) { - geoDistance = GeoDistance.fromString(parser.text()); - } else if ("coerce".equals(currentName) || "normalize".equals(currentName)) { - coerce = parser.booleanValue(); - if (coerce == true) { - ignoreMalformed = true; - } - } else if ("ignore_malformed".equals(currentName)) { - boolean ignore_malformed_value = parser.booleanValue(); - if (coerce == false) { - ignoreMalformed = ignore_malformed_value; - } - } else if ("sort_mode".equals(currentName) || "sortMode".equals(currentName) || "mode".equals(currentName)) { - sortMode = MultiValueMode.fromString(parser.text()); - } else if ("nested_path".equals(currentName) || "nestedPath".equals(currentName)) { - nestedPath = parser.text(); - } else { - GeoPoint point = new GeoPoint(); - point.resetFromString(parser.text()); - geoPoints.add(point); - fieldName = currentName; - } - } - } - - GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(fieldName, geoPoints.toArray(new GeoPoint[geoPoints.size()])); - result.geoDistance(geoDistance); - result.unit(unit); - if (reverse) { - result.order(SortOrder.DESC); - } else { - result.order(SortOrder.ASC); - } - if (sortMode != null) { - result.sortMode(sortMode.name()); - } - result.setNestedFilter(nestedFilter); - result.setNestedPath(nestedPath); - result.coerce(coerce); - result.ignoreMalformed(ignoreMalformed); - return result; - - } - - static void parseGeoPoints(XContentParser parser, List geoPoints) throws IOException { - while (!parser.nextToken().equals(XContentParser.Token.END_ARRAY)) { - if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { - // we might get here if the geo point is " number, number] " and the parser already moved over the opening bracket - // in this case we cannot use GeoUtils.parseGeoPoint(..) because this expects an opening bracket - double lon = parser.doubleValue(); - parser.nextToken(); - if (!parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)) { - throw new ElasticsearchParseException("geo point parsing: expected second number but got [{}] instead", parser.currentToken()); - } - double lat = parser.doubleValue(); - GeoPoint point = new GeoPoint(); - point.reset(lat, lon); - geoPoints.add(point); - } else { - GeoPoint point = new GeoPoint(); - GeoUtils.parseGeoPoint(parser, point); - geoPoints.add(point); - } - - } - } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java index 248a051021e..9fddf590ca4 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java +++ b/core/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java @@ -72,8 +72,8 @@ public class GeoDistanceSortParser implements SortParser { NestedInnerQueryParseSupport nestedHelper = null; final boolean indexCreatedBeforeV2_0 = context.indexShard().getIndexSettings().getIndexVersionCreated().before(Version.V_2_0_0); - boolean coerce = GeoDistanceSortBuilder.DEFAULT_COERCE; - boolean ignoreMalformed = GeoDistanceSortBuilder.DEFAULT_IGNORE_MALFORMED; + boolean coerce = false; + boolean ignoreMalformed = false; XContentParser.Token token; String currentName = parser.currentName(); @@ -81,7 +81,7 @@ public class GeoDistanceSortParser implements SortParser { if (token == XContentParser.Token.FIELD_NAME) { currentName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { - GeoDistanceSortBuilder.parseGeoPoints(parser, geoPoints); + parseGeoPoints(parser, geoPoints); fieldName = currentName; } else if (token == XContentParser.Token.START_OBJECT) { @@ -213,4 +213,26 @@ public class GeoDistanceSortParser implements SortParser { return new SortField(fieldName, geoDistanceComparatorSource, reverse); } + private void parseGeoPoints(XContentParser parser, List geoPoints) throws IOException { + while (!parser.nextToken().equals(XContentParser.Token.END_ARRAY)) { + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + // we might get here if the geo point is " number, number] " and the parser already moved over the opening bracket + // in this case we cannot use GeoUtils.parseGeoPoint(..) because this expects an opening bracket + double lon = parser.doubleValue(); + parser.nextToken(); + if (!parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)) { + throw new ElasticsearchParseException("geo point parsing: expected second number but got [{}] instead", parser.currentToken()); + } + double lat = parser.doubleValue(); + GeoPoint point = new GeoPoint(); + point.reset(lat, lon); + geoPoints.add(point); + } else { + GeoPoint point = new GeoPoint(); + GeoUtils.parseGeoPoint(parser, point); + geoPoints.add(point); + } + + } + } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java b/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java index f326fee3837..9a843c43f74 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java +++ b/core/src/main/java/org/elasticsearch/search/sort/SortBuilders.java @@ -19,11 +19,8 @@ package org.elasticsearch.search.sort; -import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.script.Script; -import java.util.Arrays; - /** * A set of static factory methods for {@link SortBuilder}s. * @@ -61,31 +58,8 @@ public class SortBuilders { * A geo distance based sort. * * @param fieldName The geo point like field name. - * @param lat Latitude of the point to create the range distance facets from. - * @param lon Longitude of the point to create the range distance facets from. - * */ - public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, double lat, double lon) { - return new GeoDistanceSortBuilder(fieldName, lat, lon); + public static GeoDistanceSortBuilder geoDistanceSort(String fieldName) { + return new GeoDistanceSortBuilder(fieldName); } - - /** - * Constructs a new distance based sort on a geo point like field. - * - * @param fieldName The geo point like field name. - * @param points The points to create the range distance facets from. - */ - public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, GeoPoint... points) { - return new GeoDistanceSortBuilder(fieldName, points); - } - - /** - * Constructs a new distance based sort on a geo point like field. - * - * @param fieldName The geo point like field name. - * @param geohashes The points to create the range distance facets from. - */ - public static GeoDistanceSortBuilder geoDistanceSort(String fieldName, String ... geohashes) { - return new GeoDistanceSortBuilder(fieldName, geohashes); - } } diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java b/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java deleted file mode 100644 index 8893471b6c1..00000000000 --- a/core/src/main/java/org/elasticsearch/search/sort/SortElementParserTemp.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.sort; - -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.index.query.QueryParseContext; - -import java.io.IOException; - -// TODO once sort refactoring is done this needs to be merged into SortBuilder -public interface SortElementParserTemp { - /** - * Creates a new SortBuilder from the json held by the {@link SortElementParserTemp} - * in {@link org.elasticsearch.common.xcontent.XContent} format - * - * @param context - * the input parse context. The state on the parser contained in - * this context will be changed as a side effect of this method - * call - * @return the new item - */ - T fromXContent(QueryParseContext context, String elementName) throws IOException; -} diff --git a/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java b/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java index 73e5ac55247..001924d1bdf 100644 --- a/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java +++ b/core/src/main/java/org/elasticsearch/search/sort/SortOrder.java @@ -51,7 +51,8 @@ public enum SortOrder implements Writeable { } }; - private static final SortOrder PROTOTYPE = ASC; + public static final SortOrder DEFAULT = DESC; + private static final SortOrder PROTOTYPE = DEFAULT; @Override public SortOrder readFrom(StreamInput in) throws IOException { diff --git a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java index d414a64f60e..bb969b90de6 100644 --- a/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java @@ -246,8 +246,8 @@ public class SearchSourceBuilderTests extends ESTestCase { builder.sort(SortBuilders.fieldSort(randomAsciiOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))); break; case 1: - builder.sort(SortBuilders.geoDistanceSort(randomAsciiOfLengthBetween(5, 20), - AbstractQueryTestCase.randomGeohash(1, 12)).order(randomFrom(SortOrder.values()))); + builder.sort(SortBuilders.geoDistanceSort(randomAsciiOfLengthBetween(5, 20)) + .geohashes(AbstractQueryTestCase.randomGeohash(1, 12)).order(randomFrom(SortOrder.values()))); break; case 2: builder.sort(SortBuilders.scoreSort().order(randomFrom(SortOrder.values()))); diff --git a/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java b/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java deleted file mode 100644 index dfea1a9316b..00000000000 --- a/core/src/test/java/org/elasticsearch/search/sort/AbstractSortTestCase.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.sort; - -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.NamedWriteable; -import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; -import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.index.query.QueryParseContext; -import org.elasticsearch.indices.query.IndicesQueriesRegistry; -import org.elasticsearch.search.SearchModule; -import org.elasticsearch.test.ESTestCase; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import java.io.IOException; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; - -public abstract class AbstractSortTestCase & ToXContent & SortElementParserTemp> extends ESTestCase { - - protected static NamedWriteableRegistry namedWriteableRegistry; - - private static final int NUMBER_OF_TESTBUILDERS = 20; - static IndicesQueriesRegistry indicesQueriesRegistry; - - @BeforeClass - public static void init() { - namedWriteableRegistry = new NamedWriteableRegistry(); - namedWriteableRegistry.registerPrototype(GeoDistanceSortBuilder.class, GeoDistanceSortBuilder.PROTOTYPE); - indicesQueriesRegistry = new SearchModule(Settings.EMPTY, namedWriteableRegistry).buildQueryParserRegistry(); - } - - @AfterClass - public static void afterClass() throws Exception { - namedWriteableRegistry = null; - } - - /** Returns random sort that is put under test */ - protected abstract T createTestItem(); - - /** Returns mutated version of original so the returned sort is different in terms of equals/hashcode */ - protected abstract T mutate(T original) throws IOException; - - /** - * Test that creates new sort from a random test sort and checks both for equality - */ - public void testFromXContent() throws IOException { - for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { - T testItem = createTestItem(); - - XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); - if (randomBoolean()) { - builder.prettyPrint(); - } - builder.startObject(); - testItem.toXContent(builder, ToXContent.EMPTY_PARAMS); - builder.endObject(); - - XContentParser itemParser = XContentHelper.createParser(builder.bytes()); - itemParser.nextToken(); - - /* - * filter out name of sort, or field name to sort on for element fieldSort - */ - itemParser.nextToken(); - String elementName = itemParser.currentName(); - itemParser.nextToken(); - - QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); - context.reset(itemParser); - NamedWriteable parsedItem = testItem.fromXContent(context, elementName); - assertNotSame(testItem, parsedItem); - assertEquals(testItem, parsedItem); - assertEquals(testItem.hashCode(), parsedItem.hashCode()); - } - } - - /** - * Test serialization and deserialization of the test sort. - */ - public void testSerialization() throws IOException { - for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { - T testsort = createTestItem(); - T deserializedsort = copyItem(testsort); - assertEquals(testsort, deserializedsort); - assertEquals(testsort.hashCode(), deserializedsort.hashCode()); - assertNotSame(testsort, deserializedsort); - } - } - - /** - * Test equality and hashCode properties - */ - public void testEqualsAndHashcode() throws IOException { - for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { - T firstsort = createTestItem(); - assertFalse("sort is equal to null", firstsort.equals(null)); - assertFalse("sort is equal to incompatible type", firstsort.equals("")); - assertTrue("sort is not equal to self", firstsort.equals(firstsort)); - assertThat("same sort's hashcode returns different values if called multiple times", firstsort.hashCode(), - equalTo(firstsort.hashCode())); - assertThat("different sorts should not be equal", mutate(firstsort), not(equalTo(firstsort))); - assertThat("different sorts should have different hashcode", mutate(firstsort).hashCode(), not(equalTo(firstsort.hashCode()))); - - T secondsort = copyItem(firstsort); - assertTrue("sort is not equal to self", secondsort.equals(secondsort)); - assertTrue("sort is not equal to its copy", firstsort.equals(secondsort)); - assertTrue("equals is not symmetric", secondsort.equals(firstsort)); - assertThat("sort copy's hashcode is different from original hashcode", secondsort.hashCode(), equalTo(firstsort.hashCode())); - - T thirdsort = copyItem(secondsort); - assertTrue("sort is not equal to self", thirdsort.equals(thirdsort)); - assertTrue("sort is not equal to its copy", secondsort.equals(thirdsort)); - assertThat("sort copy's hashcode is different from original hashcode", secondsort.hashCode(), equalTo(thirdsort.hashCode())); - assertTrue("equals is not transitive", firstsort.equals(thirdsort)); - assertThat("sort copy's hashcode is different from original hashcode", firstsort.hashCode(), equalTo(thirdsort.hashCode())); - assertTrue("equals is not symmetric", thirdsort.equals(secondsort)); - assertTrue("equals is not symmetric", thirdsort.equals(firstsort)); - } - } - - protected T copyItem(T original) throws IOException { - try (BytesStreamOutput output = new BytesStreamOutput()) { - original.writeTo(output); - try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), namedWriteableRegistry)) { - @SuppressWarnings("unchecked") - T prototype = (T) namedWriteableRegistry.getPrototype(getPrototype(), original.getWriteableName()); - T copy = (T) prototype.readFrom(in); - return copy; - } - } - } - - protected abstract Class getPrototype(); -} diff --git a/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java b/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java deleted file mode 100644 index dbb473f22ef..00000000000 --- a/core/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.sort; - - -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.geo.GeoDistance; -import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.unit.DistanceUnit; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.QueryParseContext; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.geo.RandomGeoGenerator; - -import java.io.IOException; -import java.util.Arrays; - -public class GeoDistanceSortBuilderTests extends AbstractSortTestCase { - - @Override - protected GeoDistanceSortBuilder createTestItem() { - String fieldName = randomAsciiOfLengthBetween(1, 10); - GeoDistanceSortBuilder result = null; - - int id = randomIntBetween(0, 2); - switch(id) { - case 0: - int count = randomIntBetween(1, 10); - String[] geohashes = new String[count]; - for (int i = 0; i < count; i++) { - geohashes[i] = RandomGeoGenerator.randomPoint(getRandom()).geohash(); - } - - result = new GeoDistanceSortBuilder(fieldName, geohashes); - break; - case 1: - GeoPoint pt = RandomGeoGenerator.randomPoint(getRandom()); - result = new GeoDistanceSortBuilder(fieldName, pt.getLat(), pt.getLon()); - break; - case 2: - result = new GeoDistanceSortBuilder(fieldName, points(new GeoPoint[0])); - break; - default: - throw new IllegalStateException("one of three geo initialisation strategies must be used"); - - } - if (randomBoolean()) { - result.geoDistance(geoDistance(result.geoDistance())); - } - if (randomBoolean()) { - result.unit(unit(result.unit())); - } - if (randomBoolean()) { - result.order(RandomSortDataGenerator.order(result.order())); - } - if (randomBoolean()) { - result.sortMode(mode(result.sortMode())); - } - if (randomBoolean()) { - result.setNestedFilter(RandomSortDataGenerator.nestedFilter(result.getNestedFilter())); - } - if (randomBoolean()) { - result.setNestedPath(RandomSortDataGenerator.randomAscii(result.getNestedPath())); - } - if (randomBoolean()) { - result.coerce(! result.coerce()); - } - if (randomBoolean()) { - result.ignoreMalformed(! result.ignoreMalformed()); - } - - return result; - } - - private static String mode(String original) { - String[] modes = {"MIN", "MAX", "AVG"}; - String mode = ESTestCase.randomFrom(modes); - while (mode.equals(original)) { - mode = ESTestCase.randomFrom(modes); - } - return mode; - } - - private DistanceUnit unit(DistanceUnit original) { - int id = -1; - while (id == -1 || (original != null && original.ordinal() == id)) { - id = randomIntBetween(0, DistanceUnit.values().length - 1); - } - return DistanceUnit.values()[id]; - } - - private GeoPoint[] points(GeoPoint[] original) { - GeoPoint[] result = null; - while (result == null || Arrays.deepEquals(original, result)) { - int count = randomIntBetween(1, 10); - result = new GeoPoint[count]; - for (int i = 0; i < count; i++) { - result[i] = RandomGeoGenerator.randomPoint(getRandom()); - } - } - return result; - } - - private GeoDistance geoDistance(GeoDistance original) { - int id = -1; - while (id == -1 || (original != null && original.ordinal() == id)) { - id = randomIntBetween(0, GeoDistance.values().length - 1); - } - return GeoDistance.values()[id]; - } - - @Override - protected GeoDistanceSortBuilder mutate(GeoDistanceSortBuilder original) throws IOException { - GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(original); - int parameter = randomIntBetween(0, 9); - switch (parameter) { - case 0: - while (Arrays.deepEquals(original.points(), result.points())) { - GeoPoint pt = RandomGeoGenerator.randomPoint(getRandom()); - result.point(pt.getLat(), pt.getLon()); - } - break; - case 1: - result.points(points(original.points())); - break; - case 2: - result.geoDistance(geoDistance(original.geoDistance())); - break; - case 3: - result.unit(unit(original.unit())); - break; - case 4: - result.order(RandomSortDataGenerator.order(original.order())); - break; - case 5: - result.sortMode(mode(original.sortMode())); - break; - case 6: - result.setNestedFilter(RandomSortDataGenerator.nestedFilter(original.getNestedFilter())); - break; - case 7: - result.setNestedPath(RandomSortDataGenerator.randomAscii(original.getNestedPath())); - break; - case 8: - result.coerce(! original.coerce()); - break; - case 9: - // ignore malformed will only be set if coerce is set to true - result.coerce(false); - result.ignoreMalformed(! original.ignoreMalformed()); - break; - } - return result; - - } - - @SuppressWarnings("unchecked") - @Override - protected Class getPrototype() { - return (Class) GeoDistanceSortBuilder.PROTOTYPE.getClass(); - } - - public void testSortModeSumIsRejectedInSetter() { - GeoDistanceSortBuilder builder = new GeoDistanceSortBuilder("testname", -1, -1); - GeoPoint point = RandomGeoGenerator.randomPoint(getRandom()); - builder.point(point.getLat(), point.getLon()); - try { - builder.sortMode("SUM"); - fail("sort mode sum should not be supported"); - } catch (IllegalArgumentException e) { - // all good - } - } - - public void testSortModeSumIsRejectedInJSON() throws IOException { - String json = "{\n" + - " \"testname\" : [ {\n" + - " \"lat\" : -6.046997540714173,\n" + - " \"lon\" : -51.94128329747579\n" + - " } ],\n" + - " \"unit\" : \"m\",\n" + - " \"distance_type\" : \"sloppy_arc\",\n" + - " \"reverse\" : true,\n" + - " \"mode\" : \"SUM\",\n" + - " \"coerce\" : false,\n" + - " \"ignore_malformed\" : false\n" + - "}"; - XContentParser itemParser = XContentHelper.createParser(new BytesArray(json)); - itemParser.nextToken(); - - QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); - context.reset(itemParser); - - try { - GeoDistanceSortBuilder.PROTOTYPE.fromXContent(context, ""); - fail("sort mode sum should not be supported"); - } catch (IllegalArgumentException e) { - // all good - } - } - - public void testGeoDistanceSortCanBeParsedFromGeoHash() throws IOException { - String json = "{\n" + - " \"VDcvDuFjE\" : [ \"7umzzv8eychg\", \"dmdgmt5z13uw\", \"ezu09wxw6v4c\", \"kc7s3515p6k6\", \"jgeuvjwrmfzn\", \"kcpcfj7ruyf8\" ],\n" + - " \"unit\" : \"m\",\n" + - " \"distance_type\" : \"sloppy_arc\",\n" + - " \"reverse\" : true,\n" + - " \"mode\" : \"MAX\",\n" + - " \"nested_filter\" : {\n" + - " \"ids\" : {\n" + - " \"type\" : [ ],\n" + - " \"values\" : [ ],\n" + - " \"boost\" : 5.711116\n" + - " }\n" + - " },\n" + - " \"coerce\" : false,\n" + - " \"ignore_malformed\" : true\n" + - " }"; - XContentParser itemParser = XContentHelper.createParser(new BytesArray(json)); - itemParser.nextToken(); - - QueryParseContext context = new QueryParseContext(indicesQueriesRegistry); - context.reset(itemParser); - - GeoDistanceSortBuilder result = GeoDistanceSortBuilder.PROTOTYPE.fromXContent(context, json); - assertEquals("[-19.700583312660456, -2.8225036337971687, " - + "31.537466906011105, -74.63590376079082, " - + "43.71844606474042, -5.548660643398762, " - + "-37.20467280596495, 38.71751043945551, " - + "-69.44606635719538, 84.25200328230858, " - + "-39.03717711567879, 44.74099852144718]", Arrays.toString(result.points())); - } -} diff --git a/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java b/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java deleted file mode 100644 index fcd5284119c..00000000000 --- a/core/src/test/java/org/elasticsearch/search/sort/RandomSortDataGenerator.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.sort; - -import org.apache.lucene.util.BytesRef; -import org.elasticsearch.index.query.IdsQueryBuilder; -import org.elasticsearch.index.query.MatchAllQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.TermQueryBuilder; -import org.elasticsearch.test.ESTestCase; - -public class RandomSortDataGenerator { - private RandomSortDataGenerator() { - // this is a helper class only, doesn't need a constructor - } - - public static QueryBuilder nestedFilter(QueryBuilder original) { - @SuppressWarnings("rawtypes") - QueryBuilder nested = null; - while (nested == null || nested.equals(original)) { - switch (ESTestCase.randomInt(2)) { - case 0: - nested = new MatchAllQueryBuilder(); - break; - case 1: - nested = new IdsQueryBuilder(); - break; - default: - case 2: - nested = new TermQueryBuilder(ESTestCase.randomAsciiOfLengthBetween(1, 10), ESTestCase.randomAsciiOfLengthBetween(1, 10)); - break; - } - nested.boost((float) ESTestCase.randomDoubleBetween(0, 10, false)); - } - return nested; - } - - public static String randomAscii(String original) { - String nestedPath = ESTestCase.randomAsciiOfLengthBetween(1, 10); - while (nestedPath.equals(original)) { - nestedPath = ESTestCase.randomAsciiOfLengthBetween(1, 10); - } - return nestedPath; - } - - public static String mode(String original) { - String[] modes = {"min", "max", "avg", "sum"}; - String mode = ESTestCase.randomFrom(modes); - while (mode.equals(original)) { - mode = ESTestCase.randomFrom(modes); - } - return mode; - } - - public static Object missing(Object original) { - Object missing = null; - Object otherMissing = null; - if (original instanceof BytesRef) { - otherMissing = ((BytesRef) original).utf8ToString(); - } else { - otherMissing = original; - } - - while (missing == null || missing.equals(otherMissing)) { - int missingId = ESTestCase.randomIntBetween(0, 3); - switch (missingId) { - case 0: - missing = ("_last"); - break; - case 1: - missing = ("_first"); - break; - case 2: - missing = ESTestCase.randomAsciiOfLength(10); - break; - case 3: - missing = ESTestCase.randomInt(); - break; - default: - throw new IllegalStateException("Unknown missing type."); - - } - } - return missing; - } - - public static SortOrder order(SortOrder original) { - SortOrder order = SortOrder.ASC; - if (order.equals(original)) { - return SortOrder.DESC; - } else { - return SortOrder.ASC; - } - } - -} diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java index e0aafcfdb27..f101303ee82 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoDistanceTests.java @@ -217,14 +217,14 @@ public class GeoDistanceTests extends ESIntegTestCase { // SORTING searchResponse = client().prepareSearch().setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("location", 40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("location").point(40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 7); assertOrderedSearchHits(searchResponse, "1", "3", "4", "5", "6", "2", "7"); searchResponse = client().prepareSearch().setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("location", 40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("location").point(40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 7); @@ -288,7 +288,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -301,7 +301,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc, Mode: max searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max")) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max")) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -314,7 +314,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -327,7 +327,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc, Mode: min searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min")) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min")) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -339,7 +339,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -351,7 +351,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(5301d, 10d)); searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC)) .execute().actionGet(); assertHitCount(searchResponse, 5); @@ -363,7 +363,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(4).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); assertFailures(client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).sortMode("sum")), + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).sortMode("sum")), RestStatus.BAD_REQUEST, containsString("sort_mode [sum] isn't supported for sorting by geo distance")); } @@ -399,7 +399,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.ASC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.ASC)) .execute().actionGet(); assertHitCount(searchResponse, 2); @@ -409,7 +409,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("test").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC)) + .addSort(SortBuilders.geoDistanceSort("locations").point(40.7143528, -74.0059731).order(SortOrder.DESC)) .execute().actionGet(); // Doc with missing geo point is first, is consistent with 0.20.x @@ -578,7 +578,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc SearchResponse searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.ASC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.ASC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -590,7 +590,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Asc, Mode: max searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max").setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.ASC).sortMode("max").setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -602,7 +602,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.DESC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.DESC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -614,7 +614,7 @@ public class GeoDistanceTests extends ESIntegTestCase { // Order: Desc, Mode: min searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min").setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).order(SortOrder.DESC).sortMode("min").setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -625,7 +625,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(3).sortValues()[0]).doubleValue(), closeTo(0d, 10d)); searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches")) + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches")) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -637,8 +637,8 @@ public class GeoDistanceTests extends ESIntegTestCase { searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) .addSort( - SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).setNestedPath("branches") - .sortMode("avg").order(SortOrder.DESC).setNestedPath("branches") + SortBuilders.geoDistanceSort("branches.location").setNestedPath("branches") + .point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.DESC).setNestedPath("branches") ) .execute().actionGet(); @@ -651,8 +651,8 @@ public class GeoDistanceTests extends ESIntegTestCase { searchResponse = client().prepareSearch("companies").setQuery(matchAllQuery()) .addSort( - SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).setNestedFilter(termQuery("branches.name", "brooklyn")) - .sortMode("avg").order(SortOrder.ASC).setNestedPath("branches") + SortBuilders.geoDistanceSort("branches.location").setNestedFilter(termQuery("branches.name", "brooklyn")) + .point(40.7143528, -74.0059731).sortMode("avg").order(SortOrder.ASC).setNestedPath("branches") ) .execute().actionGet(); assertHitCount(searchResponse, 4); @@ -664,7 +664,7 @@ public class GeoDistanceTests extends ESIntegTestCase { assertThat(((Number) searchResponse.getHits().getAt(3).sortValues()[0]).doubleValue(), equalTo(Double.MAX_VALUE)); assertFailures(client().prepareSearch("companies").setQuery(matchAllQuery()) - .addSort(SortBuilders.geoDistanceSort("branches.location", 40.7143528, -74.0059731).sortMode("sum").setNestedPath("branches")), + .addSort(SortBuilders.geoDistanceSort("branches.location").point(40.7143528, -74.0059731).sortMode("sum").setNestedPath("branches")), RestStatus.BAD_REQUEST, containsString("sort_mode [sum] isn't supported for sorting by geo distance")); } diff --git a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java index 506cd75a57d..b0c77f54197 100644 --- a/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java +++ b/modules/lang-groovy/src/test/java/org/elasticsearch/messy/tests/SimpleSortTests.java @@ -1759,7 +1759,7 @@ public class SimpleSortTests extends ESIntegTestCase { SearchResponse searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location", q).sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d1", "d2"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 2, 3, 2, DistanceUnit.KILOMETERS), 0.01d)); @@ -1767,7 +1767,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location", q).sortMode("min").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d2", "d1"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 1, 5, 1, DistanceUnit.KILOMETERS), 0.01d)); @@ -1775,7 +1775,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location", q).sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d1", "d2"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 2, 4, 1, DistanceUnit.KILOMETERS), 0.01d)); @@ -1783,7 +1783,7 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) - .addSort(new GeoDistanceSortBuilder("location", q).sortMode("max").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) .execute().actionGet(); assertOrderedSearchHits(searchResponse, "d2", "d1"); assertThat((Double)searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2, 1, 6, 2, DistanceUnit.KILOMETERS), 0.01d)); @@ -1835,22 +1835,13 @@ public class SimpleSortTests extends ESIntegTestCase { List qPoints = new ArrayList<>(); createQPoints(qHashes, qPoints); - - GeoDistanceSortBuilder geoDistanceSortBuilder = null; + GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); for (int i = 0; i < 4; i++) { int at = randomInt(3 - i); if (randomBoolean()) { - if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qHashes.get(at)); - } else { - geoDistanceSortBuilder.geohashes(qHashes.get(at)); - } + geoDistanceSortBuilder.geohashes(qHashes.get(at)); } else { - if (geoDistanceSortBuilder == null) { - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", qPoints.get(at)); - } else { geoDistanceSortBuilder.points(qPoints.get(at)); - } } qHashes.remove(at); qPoints.remove(at); @@ -1883,7 +1874,8 @@ public class SimpleSortTests extends ESIntegTestCase { String hashPoint = "s037ms06g7h0"; - GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", hashPoint); + GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); + geoDistanceSortBuilder.geohashes(hashPoint); SearchResponse searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1891,7 +1883,8 @@ public class SimpleSortTests extends ESIntegTestCase { .execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", new GeoPoint(2, 2)); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); + geoDistanceSortBuilder.points(new GeoPoint(2, 2)); searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1899,7 +1892,8 @@ public class SimpleSortTests extends ESIntegTestCase { .execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); - geoDistanceSortBuilder = new GeoDistanceSortBuilder("location", 2, 2); + geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); + geoDistanceSortBuilder.point(2, 2); searchResponse = client().prepareSearch() .setQuery(matchAllQuery()) @@ -1910,21 +1904,21 @@ public class SimpleSortTests extends ESIntegTestCase { searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", 2.0, 2.0) + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").point(2.0, 2.0) .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", "s037ms06g7h0") + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").geohashes("s037ms06g7h0") .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); searchResponse = client() .prepareSearch() .setSource( - new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location", 2.0, 2.0) + new SearchSourceBuilder().sort(SortBuilders.geoDistanceSort("location").point(2.0, 2.0) .unit(DistanceUnit.KILOMETERS).geoDistance(GeoDistance.PLANE))).execute().actionGet(); checkCorrectSortOrderForGeoSort(searchResponse); } From 13ad335416f725a03edbab3d33c2d0cbb4fe71c5 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 2 Feb 2016 14:08:19 +0100 Subject: [PATCH 30/49] Fix compilation in TransportReplicationActionTests --- .../replication/TransportReplicationActionTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java index df7d4c10f60..ce9343309a6 100644 --- a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java +++ b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java @@ -221,7 +221,7 @@ public class TransportReplicationActionTests extends ESTestCase { */ public void testNoRerouteOnStaleClusterState() throws InterruptedException, ExecutionException { final String index = "test"; - final ShardId shardId = new ShardId(index, 0); + final ShardId shardId = new ShardId(index, "_na_", 0); ClusterState state = state(index, true, ShardRoutingState.RELOCATING); String relocationTargetNode = state.getRoutingTable().shardRoutingTable(shardId).primaryShard().relocatingNodeId(); state = ClusterState.builder(state).nodes(DiscoveryNodes.builder(state.nodes()).localNodeId(relocationTargetNode)).build(); @@ -230,13 +230,13 @@ public class TransportReplicationActionTests extends ESTestCase { Request request = new Request(shardId).timeout("1ms").routedBasedOnClusterVersion(clusterService.state().version() + 1); PlainActionFuture listener = new PlainActionFuture<>(); - TransportReplicationAction.ReroutePhase reroutePhase = action.new ReroutePhase(request, listener); + TransportReplicationAction.ReroutePhase reroutePhase = action.new ReroutePhase(null, request, listener); reroutePhase.run(); assertListenerThrows("cluster state too old didn't cause a timeout", listener, UnavailableShardsException.class); request = new Request(shardId).routedBasedOnClusterVersion(clusterService.state().version() + 1); listener = new PlainActionFuture<>(); - reroutePhase = action.new ReroutePhase(request, listener); + reroutePhase = action.new ReroutePhase(null, request, listener); reroutePhase.run(); assertFalse("cluster state too old didn't cause a retry", listener.isDone()); From 58b6db8d824c0abcb282da7d1ca35032f991518f Mon Sep 17 00:00:00 2001 From: Clinton Gormley Date: Tue, 2 Feb 2016 14:37:57 +0100 Subject: [PATCH 31/49] Added versions 2.1.3-SNAPSHOT and 2.2.1-SNAPSHOT, and bwc indices for 2.1.2 and 2.2.0 --- .../main/java/org/elasticsearch/Version.java | 12 ++++++++++-- .../test/resources/indices/bwc/index-2.1.2.zip | Bin 0 -> 80525 bytes .../test/resources/indices/bwc/index-2.2.0.zip | Bin 0 -> 95275 bytes .../test/resources/indices/bwc/repo-2.1.2.zip | Bin 0 -> 78647 bytes .../test/resources/indices/bwc/repo-2.2.0.zip | Bin 0 -> 93162 bytes 5 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 core/src/test/resources/indices/bwc/index-2.1.2.zip create mode 100644 core/src/test/resources/indices/bwc/index-2.2.0.zip create mode 100644 core/src/test/resources/indices/bwc/repo-2.1.2.zip create mode 100644 core/src/test/resources/indices/bwc/repo-2.2.0.zip diff --git a/core/src/main/java/org/elasticsearch/Version.java b/core/src/main/java/org/elasticsearch/Version.java index a07afcc9ea5..330ce5eb99e 100644 --- a/core/src/main/java/org/elasticsearch/Version.java +++ b/core/src/main/java/org/elasticsearch/Version.java @@ -279,9 +279,13 @@ public class Version { public static final int V_2_1_1_ID = 2010199; public static final Version V_2_1_1 = new Version(V_2_1_1_ID, false, org.apache.lucene.util.Version.LUCENE_5_3_1); public static final int V_2_1_2_ID = 2010299; - public static final Version V_2_1_2 = new Version(V_2_1_2_ID, true, org.apache.lucene.util.Version.LUCENE_5_3_1); + public static final Version V_2_1_2 = new Version(V_2_1_2_ID, false, org.apache.lucene.util.Version.LUCENE_5_3_1); + public static final int V_2_1_3_ID = 2010399; + public static final Version V_2_1_3 = new Version(V_2_1_3_ID, true, org.apache.lucene.util.Version.LUCENE_5_3_1); public static final int V_2_2_0_ID = 2020099; - public static final Version V_2_2_0 = new Version(V_2_2_0_ID, true, org.apache.lucene.util.Version.LUCENE_5_4_0); + public static final Version V_2_2_0 = new Version(V_2_2_0_ID, false, org.apache.lucene.util.Version.LUCENE_5_4_0); + public static final int V_2_2_1_ID = 2020199; + public static final Version V_2_2_1 = new Version(V_2_2_1_ID, true, org.apache.lucene.util.Version.LUCENE_5_4_0); public static final int V_2_3_0_ID = 2030099; public static final Version V_2_3_0 = new Version(V_2_3_0_ID, true, org.apache.lucene.util.Version.LUCENE_5_4_0); public static final int V_3_0_0_ID = 3000099; @@ -303,8 +307,12 @@ public class Version { return V_3_0_0; case V_2_3_0_ID: return V_2_3_0; + case V_2_2_1_ID: + return V_2_2_1; case V_2_2_0_ID: return V_2_2_0; + case V_2_1_3_ID: + return V_2_1_3; case V_2_1_2_ID: return V_2_1_2; case V_2_1_1_ID: diff --git a/core/src/test/resources/indices/bwc/index-2.1.2.zip b/core/src/test/resources/indices/bwc/index-2.1.2.zip new file mode 100644 index 0000000000000000000000000000000000000000..739c104a236d8ccfdcd81b5bbb6c3b0cf4ad4d84 GIT binary patch literal 80525 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ívFRSL8zJU8rEL%`1-f{Z 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 HG_?N%LiYzV literal 0 HcmV?d00001 diff --git a/core/src/test/resources/indices/bwc/index-2.2.0.zip b/core/src/test/resources/indices/bwc/index-2.2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..f9011ffce07105c5fb4fe6ab601eb0233baa29f6 GIT binary patch literal 95275 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

F7~=KMcd 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

`-xF`FBN`-M;cBsM# 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 literal 0 HcmV?d00001 diff --git a/core/src/test/resources/indices/bwc/repo-2.1.2.zip b/core/src/test/resources/indices/bwc/repo-2.1.2.zip new file mode 100644 index 0000000000000000000000000000000000000000..a89507f00423f05e18cbfcd09c9d2d8bcb49484f GIT binary patch literal 78647 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 z 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 Zf2<)WXwZcgfDQU-hXnvk|8kQ7{|_u7faU-I literal 0 HcmV?d00001 diff --git a/core/src/test/resources/indices/bwc/repo-2.2.0.zip b/core/src/test/resources/indices/bwc/repo-2.2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..90f8282f46abebca887fa9b026a2f63c40a49006 GIT binary patch literal 93162 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

Zcep88|!)WHM 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Ė^)hjmh8lifqJS1!`w>go>F)S5VLPeJe)2M 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 literal 0 HcmV?d00001 From 69a3f7f590afb3e29b33f9918175e8d1e4004e64 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 2 Feb 2016 08:26:30 -0500 Subject: [PATCH 32/49] Add random positive time value convenience method This commit adds a convenience method for producing random positive time values which can be useful for places where non-negative and non-zero time values are expected. --- .../java/org/elasticsearch/test/ESTestCase.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 67cae85f4c3..e80bb93aeb7 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -382,9 +382,18 @@ public abstract class ESTestCase extends LuceneTestCase { return generateRandomStringArray(maxArraySize, maxStringSize, allowNull, true); } + private static String[] TIME_SUFFIXES = new String[]{"d", "H", "ms", "s", "S", "w"}; + + private static String randomTimeValue(int lower, int upper) { + return randomIntBetween(lower, upper) + randomFrom(TIME_SUFFIXES); + } + public static String randomTimeValue() { - final String[] values = new String[]{"d", "H", "ms", "s", "S", "w"}; - return randomIntBetween(0, 1000) + randomFrom(values); + return randomTimeValue(0, 1000); + } + + public static String randomPositiveTimeValue() { + return randomTimeValue(1, 1000); } /** From 1e6b2d4f1d21442b176bff81279231316ec1ad23 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 2 Feb 2016 08:27:57 -0500 Subject: [PATCH 33/49] Fix JVM GC monitor missing settings test This commit fixes a test bug in JvmGcMonitorServiceSettingsTests#testMissingSetting. The purpose of the test is to test that if settings are provided for a collector for at least one of warn, info, and debug then it is provided for all of warn, info, and debug. However, for a collector setting to be valid it must be a positive time value but the randomization in the test construction could produce zero time values. Closes #16369 --- .../monitor/jvm/JvmGcMonitorServiceSettingsTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/org/elasticsearch/monitor/jvm/JvmGcMonitorServiceSettingsTests.java b/core/src/test/java/org/elasticsearch/monitor/jvm/JvmGcMonitorServiceSettingsTests.java index 29f497458c7..2c248969b2c 100644 --- a/core/src/test/java/org/elasticsearch/monitor/jvm/JvmGcMonitorServiceSettingsTests.java +++ b/core/src/test/java/org/elasticsearch/monitor/jvm/JvmGcMonitorServiceSettingsTests.java @@ -62,9 +62,9 @@ public class JvmGcMonitorServiceSettingsTests extends ESTestCase { public void testMissingSetting() throws InterruptedException { String collector = randomAsciiOfLength(5); Set> entries = new HashSet<>(); - entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".warn", randomTimeValue())); - entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".info", randomTimeValue())); - entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".debug", randomTimeValue())); + entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".warn", randomPositiveTimeValue())); + entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".info", randomPositiveTimeValue())); + entries.add(new AbstractMap.SimpleEntry<>("monitor.jvm.gc.collector." + collector + ".debug", randomPositiveTimeValue())); Settings.Builder builder = Settings.builder(); // drop a random setting or two From 53662b0be9f0f4675197b3565c6158a35c249f1d Mon Sep 17 00:00:00 2001 From: Clinton Gormley Date: Tue, 2 Feb 2016 15:12:54 +0100 Subject: [PATCH 34/49] Merge pull request #16345 from lbrito1/patch-1 Changes "that is" to "for example". --- docs/reference/aggregations/pipeline.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/aggregations/pipeline.asciidoc b/docs/reference/aggregations/pipeline.asciidoc index e4cdae5a781..540f4c25f79 100644 --- a/docs/reference/aggregations/pipeline.asciidoc +++ b/docs/reference/aggregations/pipeline.asciidoc @@ -22,7 +22,7 @@ parameter to indicate the paths to the required metrics. The syntax for defining Pipeline aggregations cannot have sub-aggregations but depending on the type it can reference another pipeline in the `buckets_path` allowing pipeline aggregations to be chained. For example, you can chain together two derivatives to calculate the second derivative -(e.g. a derivative of a derivative). +(i.e. a derivative of a derivative). NOTE: Because pipeline aggregations only add to the output, when chaining pipeline aggregations the output of each pipeline aggregation will be included in the final output. From f640bf06369ec7319b78a9033a5d64b6b854283a Mon Sep 17 00:00:00 2001 From: Clinton Gormley Date: Tue, 2 Feb 2016 15:22:14 +0100 Subject: [PATCH 35/49] Merge pull request #16371 from clintongormley/deprecate-multicast Deprecate the discovery-multicast plugin --- docs/reference/migration/migrate_2_2.asciidoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/reference/migration/migrate_2_2.asciidoc b/docs/reference/migration/migrate_2_2.asciidoc index efb063f7c0f..39c059e7f47 100644 --- a/docs/reference/migration/migrate_2_2.asciidoc +++ b/docs/reference/migration/migrate_2_2.asciidoc @@ -48,3 +48,8 @@ Proxy settings have been deprecated and renamed: If you are using proxy settings, update your settings as deprecated ones will be removed in next major version. +[float] +=== Multicast plugin deprecated + +The `discovery-multicast` plugin has been deprecated in 2.2.0 and has +been removed in 3.0.0. From e6a5e79ede39b7a9c1ba4d372b63438a18aea113 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 2 Feb 2016 09:50:07 -0500 Subject: [PATCH 36/49] Test awaits Lucene snapshot upgrade This commit marks OldIndexBackwardsCompatibilityIT#testOldIndexes as awaiting a Lucene snapshot upgrade to reflect the fact that Elasticsearch 2.2.0 is built against Lucene 5.4.1 but the current Lucene snapshot in master/2.x does not contain the Lucene version 5.4.1 field. Relates #16373 --- .../elasticsearch/bwcompat/OldIndexBackwardsCompatibilityIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/java/org/elasticsearch/bwcompat/OldIndexBackwardsCompatibilityIT.java b/core/src/test/java/org/elasticsearch/bwcompat/OldIndexBackwardsCompatibilityIT.java index b5b1f955ae0..d23e11e2fb3 100644 --- a/core/src/test/java/org/elasticsearch/bwcompat/OldIndexBackwardsCompatibilityIT.java +++ b/core/src/test/java/org/elasticsearch/bwcompat/OldIndexBackwardsCompatibilityIT.java @@ -279,6 +279,7 @@ public class OldIndexBackwardsCompatibilityIT extends ESIntegTestCase { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/16373") public void testOldIndexes() throws Exception { setupCluster(); From cf28d62fc41b607c81d2ca4a7d30642d322c9401 Mon Sep 17 00:00:00 2001 From: Yannick Welsch Date: Tue, 2 Feb 2016 16:07:13 +0100 Subject: [PATCH 37/49] =?UTF-8?q?[TEST]=C2=A0Fail=20test=20if=20dummy=20do?= =?UTF-8?q?c=20is=20not=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts back 7d3da91 after fix in #15900 Closes #8706 --- .../main/java/org/elasticsearch/test/ESIntegTestCase.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index cddf4632cf8..39508644361 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -1435,11 +1435,8 @@ public abstract class ESIntegTestCase extends ESTestCase { if (!bogusIds.isEmpty()) { // delete the bogus types again - it might trigger merges or at least holes in the segments and enforces deleted docs! for (Tuple doc : bogusIds) { - // see https://github.com/elasticsearch/elasticsearch/issues/8706 - final DeleteResponse deleteResponse = client().prepareDelete(doc.v1(), RANDOM_BOGUS_TYPE, doc.v2()).get(); - if (deleteResponse.isFound() == false) { - logger.warn("failed to delete a dummy doc [{}][{}]", doc.v1(), doc.v2()); - } + assertTrue("failed to delete a dummy doc [" + doc.v1() + "][" + doc.v2() + "]", + client().prepareDelete(doc.v1(), RANDOM_BOGUS_TYPE, doc.v2()).get().isFound()); } } if (forceRefresh) { From 865bbc209604fee308552b95b62b288f003fe8e5 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 2 Feb 2016 12:01:39 +0100 Subject: [PATCH 38/49] Remove string formatting from Terminal print methods This can be trappy and wrong formating strings can throws format exceptions and hide the real message. --- .../bootstrap/BootstrapCLIParser.java | 4 ++- .../common/cli/CheckFileCommand.java | 9 ++--- .../org/elasticsearch/common/cli/CliTool.java | 4 +-- .../elasticsearch/common/cli/Terminal.java | 36 +++++++++---------- .../internal/InternalSettingsPreparer.java | 4 +-- .../plugins/InstallPluginCommand.java | 28 +++++++-------- .../elasticsearch/plugins/PluginSecurity.java | 2 +- .../plugins/RemovePluginCommand.java | 16 ++++----- .../common/cli/TerminalTests.java | 8 +++-- .../mapper/attachments/StandaloneRunner.java | 5 ++- .../common/cli/CliToolTestCase.java | 17 ++++----- 11 files changed, 66 insertions(+), 67 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java index c89ca8ea3dd..9cae3b8cb15 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCLIParser.java @@ -83,7 +83,9 @@ final class BootstrapCLIParser extends CliTool { @Override public ExitStatus execute(Settings settings, Environment env) throws Exception { - terminal.println("Version: %s, Build: %s/%s, JVM: %s", org.elasticsearch.Version.CURRENT, Build.CURRENT.shortHash(), Build.CURRENT.date(), JvmInfo.jvmInfo().version()); + terminal.println("Version: " + org.elasticsearch.Version.CURRENT + + ", Build: " + Build.CURRENT.shortHash() + "/" + Build.CURRENT.date() + + ", JVM: " + JvmInfo.jvmInfo().version()); return ExitStatus.OK_AND_EXIT; } } diff --git a/core/src/main/java/org/elasticsearch/common/cli/CheckFileCommand.java b/core/src/main/java/org/elasticsearch/common/cli/CheckFileCommand.java index 9635ae5e3ec..95502f16700 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/CheckFileCommand.java +++ b/core/src/main/java/org/elasticsearch/common/cli/CheckFileCommand.java @@ -100,8 +100,9 @@ public abstract class CheckFileCommand extends CliTool.Command { Set permissionsBeforeWrite = entry.getValue(); Set permissionsAfterWrite = Files.getPosixFilePermissions(entry.getKey()); if (!permissionsBeforeWrite.equals(permissionsAfterWrite)) { - terminal.printWarn("The file permissions of [%s] have changed from [%s] to [%s]", - entry.getKey(), PosixFilePermissions.toString(permissionsBeforeWrite), PosixFilePermissions.toString(permissionsAfterWrite)); + terminal.printWarn("The file permissions of [" + entry.getKey() + "] have changed " + + "from [" + PosixFilePermissions.toString(permissionsBeforeWrite) + "] " + + "to [" + PosixFilePermissions.toString(permissionsAfterWrite) + "]"); terminal.printWarn("Please ensure that the user account running Elasticsearch has read access to this file!"); } } @@ -115,7 +116,7 @@ public abstract class CheckFileCommand extends CliTool.Command { String ownerBeforeWrite = entry.getValue(); String ownerAfterWrite = Files.getOwner(entry.getKey()).getName(); if (!ownerAfterWrite.equals(ownerBeforeWrite)) { - terminal.printWarn("WARN: Owner of file [%s] used to be [%s], but now is [%s]", entry.getKey(), ownerBeforeWrite, ownerAfterWrite); + terminal.printWarn("WARN: Owner of file [" + entry.getKey() + "] used to be [" + ownerBeforeWrite + "], but now is [" + ownerAfterWrite + "]"); } } @@ -128,7 +129,7 @@ public abstract class CheckFileCommand extends CliTool.Command { String groupBeforeWrite = entry.getValue(); String groupAfterWrite = Files.readAttributes(entry.getKey(), PosixFileAttributes.class).group().getName(); if (!groupAfterWrite.equals(groupBeforeWrite)) { - terminal.printWarn("WARN: Group of file [%s] used to be [%s], but now is [%s]", entry.getKey(), groupBeforeWrite, groupAfterWrite); + terminal.printWarn("WARN: Group of file [" + entry.getKey() + "] used to be [" + groupBeforeWrite + "], but now is [" + groupAfterWrite + "]"); } } diff --git a/core/src/main/java/org/elasticsearch/common/cli/CliTool.java b/core/src/main/java/org/elasticsearch/common/cli/CliTool.java index 7e954bc85ee..17994eba439 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/CliTool.java +++ b/core/src/main/java/org/elasticsearch/common/cli/CliTool.java @@ -20,7 +20,6 @@ package org.elasticsearch.common.cli; import org.apache.commons.cli.AlreadySelectedException; -import org.apache.commons.cli.AmbiguousOptionException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -31,7 +30,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.node.internal.InternalSettingsPreparer; -import java.io.IOException; import java.util.Locale; import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; @@ -127,7 +125,7 @@ public abstract class CliTool { String cmdName = args[0]; cmd = config.cmd(cmdName); if (cmd == null) { - terminal.printError("unknown command [%s]. Use [-h] option to list available commands", cmdName); + terminal.printError("unknown command [" + cmdName + "]. Use [-h] option to list available commands"); return ExitStatus.USAGE; } diff --git a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java index 7608c9812de..68229f69c33 100644 --- a/core/src/main/java/org/elasticsearch/common/cli/Terminal.java +++ b/core/src/main/java/org/elasticsearch/common/cli/Terminal.java @@ -89,37 +89,37 @@ public abstract class Terminal { println(Verbosity.NORMAL); } - public void println(String msg, Object... args) { - println(Verbosity.NORMAL, msg, args); + public void println(String msg) { + println(Verbosity.NORMAL, msg); } - public void print(String msg, Object... args) { - print(Verbosity.NORMAL, msg, args); + public void print(String msg) { + print(Verbosity.NORMAL, msg); } public void println(Verbosity verbosity) { println(verbosity, ""); } - public void println(Verbosity verbosity, String msg, Object... args) { - print(verbosity, msg + System.lineSeparator(), args); + public void println(Verbosity verbosity, String msg) { + print(verbosity, msg + System.lineSeparator()); } - public void print(Verbosity verbosity, String msg, Object... args) { + public void print(Verbosity verbosity, String msg) { if (this.verbosity.enabled(verbosity)) { - doPrint(msg, args); + doPrint(msg); } } - public void printError(String msg, Object... args) { - println(Verbosity.SILENT, "ERROR: " + msg, args); + public void printError(String msg) { + println(Verbosity.SILENT, "ERROR: " + msg); } - public void printWarn(String msg, Object... args) { - println(Verbosity.SILENT, "WARN: " + msg, args); + public void printWarn(String msg) { + println(Verbosity.SILENT, "WARN: " + msg); } - protected abstract void doPrint(String msg, Object... args); + protected abstract void doPrint(String msg); private static class ConsoleTerminal extends Terminal { @@ -130,8 +130,8 @@ public abstract class Terminal { } @Override - public void doPrint(String msg, Object... args) { - console.printf(msg, args); + public void doPrint(String msg) { + console.printf("%s", msg); console.flush(); } @@ -157,13 +157,13 @@ public abstract class Terminal { private final PrintWriter printWriter = new PrintWriter(System.out); @Override - public void doPrint(String msg, Object... args) { - System.out.print(String.format(Locale.ROOT, msg, args)); + public void doPrint(String msg) { + System.out.print(msg); } @Override public String readText(String text, Object... args) { - print(text, args); + print(text); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); try { return reader.readLine(); diff --git a/core/src/main/java/org/elasticsearch/node/internal/InternalSettingsPreparer.java b/core/src/main/java/org/elasticsearch/node/internal/InternalSettingsPreparer.java index c5dd64a67bb..34b6d07e419 100644 --- a/core/src/main/java/org/elasticsearch/node/internal/InternalSettingsPreparer.java +++ b/core/src/main/java/org/elasticsearch/node/internal/InternalSettingsPreparer.java @@ -247,8 +247,8 @@ public class InternalSettingsPreparer { } if (secret) { - return new String(terminal.readSecret("Enter value for [%s]: ", key)); + return new String(terminal.readSecret("Enter value for [" + key + "]: ", key)); } - return terminal.readText("Enter value for [%s]: ", key); + return terminal.readText("Enter value for [" + key + "]: ", key); } } diff --git a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 5600d407948..697f99014dc 100644 --- a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -19,6 +19,18 @@ package org.elasticsearch.plugins; +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.bootstrap.JarHell; +import org.elasticsearch.common.cli.CliTool; +import org.elasticsearch.common.cli.Terminal; +import org.elasticsearch.common.cli.UserError; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.io.FileSystemUtils; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -42,18 +54,6 @@ import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import org.apache.lucene.util.IOUtils; -import org.elasticsearch.Build; -import org.elasticsearch.Version; -import org.elasticsearch.bootstrap.JarHell; -import org.elasticsearch.common.cli.CliTool; -import org.elasticsearch.common.cli.Terminal; -import org.elasticsearch.common.cli.UserError; -import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.common.io.FileSystemUtils; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; - import static java.util.Collections.unmodifiableSet; import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE; import static org.elasticsearch.common.util.set.Sets.newHashSet; @@ -133,7 +133,7 @@ class InstallPluginCommand extends CliTool.Command { // TODO: remove this leniency!! is it needed anymore? if (Files.exists(env.pluginsFile()) == false) { - terminal.println("Plugins directory [%s] does not exist. Creating...", env.pluginsFile()); + terminal.println("Plugins directory [" + env.pluginsFile() + "] does not exist. Creating..."); Files.createDirectory(env.pluginsFile()); } @@ -242,7 +242,7 @@ class InstallPluginCommand extends CliTool.Command { private PluginInfo verify(Path pluginRoot, Environment env) throws Exception { // read and validate the plugin descriptor PluginInfo info = PluginInfo.readFromProperties(pluginRoot); - terminal.println(VERBOSE, "%s", info); + terminal.println(VERBOSE, info.toString()); // don't let luser install plugin as a module... // they might be unavoidably in maven central and are packaged up the same way) diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java index 4fd039cfaaf..9bbafa6e16a 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java @@ -87,7 +87,7 @@ class PluginSecurity { terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); // print all permissions: for (Permission permission : requested) { - terminal.println(Verbosity.NORMAL, "* %s", formatPermission(permission)); + terminal.println(Verbosity.NORMAL, "* " + formatPermission(permission)); } terminal.println(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"); terminal.println(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); diff --git a/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java index d8fd0b8f250..8ce1056bbfd 100644 --- a/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/RemovePluginCommand.java @@ -19,12 +19,6 @@ package org.elasticsearch.plugins; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; - import org.apache.lucene.util.IOUtils; import org.elasticsearch.common.Strings; import org.elasticsearch.common.cli.CliTool; @@ -33,6 +27,12 @@ import org.elasticsearch.common.cli.UserError; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE; /** @@ -63,10 +63,10 @@ class RemovePluginCommand extends CliTool.Command { throw new UserError(CliTool.ExitStatus.IO_ERROR, "Bin dir for " + pluginName + " is not a directory"); } pluginPaths.add(pluginBinDir); - terminal.println(VERBOSE, "Removing: %s", pluginBinDir); + terminal.println(VERBOSE, "Removing: " + pluginBinDir); } - terminal.println(VERBOSE, "Removing: %s", pluginDir); + terminal.println(VERBOSE, "Removing: " + pluginDir); Path tmpPluginDir = env.pluginsFile().resolve(".removing-" + pluginName); Files.move(pluginDir, tmpPluginDir, StandardCopyOption.ATOMIC_MOVE); pluginPaths.add(tmpPluginDir); diff --git a/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java b/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java index 498dd269a23..3f5562fff61 100644 --- a/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java +++ b/core/src/test/java/org/elasticsearch/common/cli/TerminalTests.java @@ -19,9 +19,6 @@ package org.elasticsearch.common.cli; -import java.nio.file.NoSuchFileException; -import java.util.List; - import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; @@ -46,6 +43,11 @@ public class TerminalTests extends CliToolTestCase { assertPrinted(terminal, Terminal.Verbosity.VERBOSE, "text"); } + public void testEscaping() throws Exception { + CaptureOutputTerminal terminal = new CaptureOutputTerminal(Terminal.Verbosity.NORMAL); + assertPrinted(terminal, Terminal.Verbosity.NORMAL, "This message contains percent like %20n"); + } + private void assertPrinted(CaptureOutputTerminal logTerminal, Terminal.Verbosity verbosity, String text) { logTerminal.print(verbosity, text); assertThat(logTerminal.getTerminalOutput(), hasSize(1)); diff --git a/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java b/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java index c94b5e0b43a..03c6e65047a 100644 --- a/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java +++ b/plugins/mapper-attachments/src/test/java/org/elasticsearch/mapper/attachments/StandaloneRunner.java @@ -36,7 +36,6 @@ import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperParser; import org.elasticsearch.index.mapper.ParseContext; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -119,7 +118,7 @@ public class StandaloneRunner extends CliTool { terminal.println("## Extracted text"); terminal.println("--------------------- BEGIN -----------------------"); - terminal.println("%s", doc.get("file.content")); + terminal.println(doc.get("file.content")); terminal.println("---------------------- END ------------------------"); terminal.println("## Metadata"); printMetadataContent(doc, AttachmentMapper.FieldNames.AUTHOR); @@ -135,7 +134,7 @@ public class StandaloneRunner extends CliTool { } private void printMetadataContent(ParseContext.Document doc, String field) { - terminal.println("- %s: %s", field, doc.get(docMapper.mappers().getMapper("file." + field).fieldType().name())); + terminal.println("- " + field + ":" + doc.get(docMapper.mappers().getMapper("file." + field).fieldType().name())); } public static byte[] copyToBytes(Path path) throws IOException { diff --git a/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java b/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java index 5dfaef187ad..35c08977ea7 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/common/cli/CliToolTestCase.java @@ -28,11 +28,8 @@ import org.junit.After; import org.junit.Before; import java.io.IOException; -import java.io.PrintWriter; -import java.io.Writer; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; @@ -73,7 +70,7 @@ public abstract class CliToolTestCase extends ESTestCase { } @Override - protected void doPrint(String msg, Object... args) { + protected void doPrint(String msg) { } @Override @@ -87,7 +84,7 @@ public abstract class CliToolTestCase extends ESTestCase { } @Override - public void print(String msg, Object... args) { + public void print(String msg) { } @Override @@ -99,7 +96,7 @@ public abstract class CliToolTestCase extends ESTestCase { */ public static class CaptureOutputTerminal extends MockTerminal { - List terminalOutput = new ArrayList(); + List terminalOutput = new ArrayList<>(); public CaptureOutputTerminal() { super(Verbosity.NORMAL); @@ -110,13 +107,13 @@ public abstract class CliToolTestCase extends ESTestCase { } @Override - protected void doPrint(String msg, Object... args) { - terminalOutput.add(String.format(Locale.ROOT, msg, args)); + protected void doPrint(String msg) { + terminalOutput.add(msg); } @Override - public void print(String msg, Object... args) { - doPrint(msg, args); + public void print(String msg) { + doPrint(msg); } @Override From 791417ca8988f73246462d22bb6a0ad9d1e08d0d Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Tue, 2 Feb 2016 11:48:19 -0800 Subject: [PATCH 39/49] Fix test file name. --- .../painless/{NoSemiColonTest.java => NoSemiColonTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename plugins/lang-painless/src/test/java/org/elasticsearch/painless/{NoSemiColonTest.java => NoSemiColonTests.java} (99%) diff --git a/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java b/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java similarity index 99% rename from plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java rename to plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java index b589f9765cd..e9c399e1eff 100644 --- a/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTest.java +++ b/plugins/lang-painless/src/test/java/org/elasticsearch/painless/NoSemiColonTests.java @@ -22,7 +22,7 @@ package org.elasticsearch.painless; import java.util.HashMap; import java.util.Map; -public class NoSemiColonTest extends ScriptTestCase { +public class NoSemiColonTests extends ScriptTestCase { public void testIfStatement() { assertEquals(1, exec("int x = 5 if (x == 5) return 1 return 0")); From 9f47b376dadebf63807a6d7b0d2bfe3341c042e7 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Tue, 2 Feb 2016 12:17:56 -0800 Subject: [PATCH 40/49] Plugin cli: Improve maven coordinates detection Identifying when a plugin id is maven coordinates is currently done by checking if the plugin id contains 2 colons. However, a valid url could have 2 colons, for example when a port is specified. This change adds another check, ensuring the plugin id with maven coordinates does not contain a slash, which only a url would have. closes #16376 --- .../org/elasticsearch/plugins/InstallPluginCommand.java | 4 ++-- .../elasticsearch/plugins/InstallPluginCommandTests.java | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 697f99014dc..f9300d87dc0 100644 --- a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -160,9 +160,9 @@ class InstallPluginCommand extends CliTool.Command { return downloadZipAndChecksum(url, tmpDir); } - // now try as maven coordinates, a valid URL would only have a single colon + // now try as maven coordinates, a valid URL would only have a colon and slash String[] coordinates = pluginId.split(":"); - if (coordinates.length == 3) { + if (coordinates.length == 3 && pluginId.contains("/") == false) { String mavenUrl = String.format(Locale.ROOT, "https://repo1.maven.org/maven2/%1$s/%2$s/%3$s/%2$s-%3$s.zip", coordinates[0].replace(".", "/") /* groupId */, coordinates[1] /* artifactId */, coordinates[2] /* version */); terminal.println("-> Downloading " + pluginId + " from maven central"); diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java index b61ba3c13ef..727728f84ab 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.plugins; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; @@ -205,6 +206,14 @@ public class InstallPluginCommandTests extends ESTestCase { assertPlugin("fake", pluginDir, env); } + public void testMalformedUrlNotMaven() throws Exception { + // has two colons, so it appears similar to maven coordinates + MalformedURLException e = expectThrows(MalformedURLException.class, () -> { + installPlugin("://host:1234", createEnv()); + }); + assertTrue(e.getMessage(), e.getMessage().contains("no protocol")); + } + public void testPluginsDirMissing() throws Exception { Environment env = createEnv(); Files.delete(env.pluginsFile()); From 54011da9502a1c06d55f338a62c3eb908979c11d Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Tue, 2 Feb 2016 12:26:21 -0800 Subject: [PATCH 41/49] Fix imports. --- .../elasticsearch/painless/PainlessLexer.java | 81 ++++++------- .../painless/PainlessParser.java | 110 ++++++++++-------- 2 files changed, 103 insertions(+), 88 deletions(-) diff --git a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java index 941615958a7..a7cf506da0d 100644 --- a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java +++ b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessLexer.java @@ -1,16 +1,19 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless; - import java.util.Set; - -import org.antlr.v4.runtime.Lexer; import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.Token; -import org.antlr.v4.runtime.TokenStream; -import org.antlr.v4.runtime.*; -import org.antlr.v4.runtime.atn.*; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.RuntimeMetaData; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.VocabularyImpl; +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.ATNDeserializer; +import org.antlr.v4.runtime.atn.LexerATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.misc.*; + +import java.util.Set; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) class PainlessLexer extends Lexer { @@ -20,14 +23,14 @@ class PainlessLexer extends Lexer { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, - BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, - MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, - LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, - BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, - AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, - ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, + BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, + MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, + LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, + BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, + AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, + ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, CHAR=68, TRUE=69, FALSE=70, NULL=71, TYPE=72, ID=73, EXTINTEGER=74, EXTID=75; public static final int EXT = 1; public static String[] modeNames = { @@ -35,36 +38,36 @@ class PainlessLexer extends Lexer { }; public static final String[] ruleNames = { - "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", - "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", - "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", + "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", "TYPE", "GENERIC", "ID", "EXTINTEGER", "EXTID" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", - "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", - "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", - "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", - "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", - "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", - "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", + "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", + "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", + "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", + "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", + "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", + "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", - "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", "TYPE", "ID", "EXTINTEGER", "EXTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); diff --git a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java index 3fd4b4075d5..53c6eb38446 100644 --- a/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java +++ b/plugins/lang-painless/src/main/java/org/elasticsearch/painless/PainlessParser.java @@ -1,13 +1,25 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless; -import org.antlr.v4.runtime.atn.*; + +import org.antlr.v4.runtime.FailedPredicateException; +import org.antlr.v4.runtime.NoViableAltException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.RuntimeMetaData; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.VocabularyImpl; +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.ATNDeserializer; +import org.antlr.v4.runtime.atn.ParserATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.*; -import org.antlr.v4.runtime.misc.*; -import org.antlr.v4.runtime.tree.*; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.antlr.v4.runtime.tree.TerminalNode; + import java.util.List; -import java.util.Iterator; -import java.util.ArrayList; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) class PainlessParser extends Parser { @@ -17,48 +29,48 @@ class PainlessParser extends Parser { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, - BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, - MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, - LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, - BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, - AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, - ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + COMMA=10, SEMICOLON=11, IF=12, ELSE=13, WHILE=14, DO=15, FOR=16, CONTINUE=17, + BREAK=18, RETURN=19, NEW=20, TRY=21, CATCH=22, THROW=23, BOOLNOT=24, BWNOT=25, + MUL=26, DIV=27, REM=28, ADD=29, SUB=30, LSH=31, RSH=32, USH=33, LT=34, + LTE=35, GT=36, GTE=37, EQ=38, EQR=39, NE=40, NER=41, BWAND=42, BWXOR=43, + BWOR=44, BOOLAND=45, BOOLOR=46, COND=47, COLON=48, INCR=49, DECR=50, ASSIGN=51, + AADD=52, ASUB=53, AMUL=54, ADIV=55, AREM=56, AAND=57, AXOR=58, AOR=59, + ALSH=60, ARSH=61, AUSH=62, OCTAL=63, HEX=64, INTEGER=65, DECIMAL=66, STRING=67, CHAR=68, TRUE=69, FALSE=70, NULL=71, TYPE=72, ID=73, EXTINTEGER=74, EXTID=75; public static final int - RULE_source = 0, RULE_statement = 1, RULE_block = 2, RULE_empty = 3, RULE_emptyscope = 4, - RULE_initializer = 5, RULE_afterthought = 6, RULE_declaration = 7, RULE_decltype = 8, - RULE_declvar = 9, RULE_trap = 10, RULE_expression = 11, RULE_extstart = 12, - RULE_extprec = 13, RULE_extcast = 14, RULE_extbrace = 15, RULE_extdot = 16, - RULE_exttype = 17, RULE_extcall = 18, RULE_extvar = 19, RULE_extfield = 20, + RULE_source = 0, RULE_statement = 1, RULE_block = 2, RULE_empty = 3, RULE_emptyscope = 4, + RULE_initializer = 5, RULE_afterthought = 6, RULE_declaration = 7, RULE_decltype = 8, + RULE_declvar = 9, RULE_trap = 10, RULE_expression = 11, RULE_extstart = 12, + RULE_extprec = 13, RULE_extcast = 14, RULE_extbrace = 15, RULE_extdot = 16, + RULE_exttype = 17, RULE_extcall = 18, RULE_extvar = 19, RULE_extfield = 20, RULE_extnew = 21, RULE_extstring = 22, RULE_arguments = 23, RULE_increment = 24; public static final String[] ruleNames = { - "source", "statement", "block", "empty", "emptyscope", "initializer", - "afterthought", "declaration", "decltype", "declvar", "trap", "expression", - "extstart", "extprec", "extcast", "extbrace", "extdot", "exttype", "extcall", + "source", "statement", "block", "empty", "emptyscope", "initializer", + "afterthought", "declaration", "decltype", "declvar", "trap", "expression", + "extstart", "extprec", "extcast", "extbrace", "extdot", "exttype", "extcall", "extvar", "extfield", "extnew", "extstring", "arguments", "increment" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", - "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", - "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", - "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", - "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", - "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", - "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "','", + "';'", "'if'", "'else'", "'while'", "'do'", "'for'", "'continue'", "'break'", + "'return'", "'new'", "'try'", "'catch'", "'throw'", "'!'", "'~'", "'*'", + "'/'", "'%'", "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", + "'>='", "'=='", "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", + "'||'", "'?'", "':'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", "'/='", + "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", - "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", - "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", - "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", - "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", - "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", - "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "COMMA", "SEMICOLON", "IF", "ELSE", "WHILE", "DO", "FOR", "CONTINUE", + "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "BOOLNOT", "BWNOT", + "MUL", "DIV", "REM", "ADD", "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", + "GTE", "EQ", "EQR", "NE", "NER", "BWAND", "BWXOR", "BWOR", "BOOLAND", + "BOOLOR", "COND", "COLON", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", + "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", + "HEX", "INTEGER", "DECIMAL", "STRING", "CHAR", "TRUE", "FALSE", "NULL", "TYPE", "ID", "EXTINTEGER", "EXTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @@ -136,7 +148,7 @@ class PainlessParser extends Parser { try { enterOuterAlt(_localctx, 1); { - setState(51); + setState(51); _errHandler.sync(this); _la = _input.LA(1); do { @@ -146,7 +158,7 @@ class PainlessParser extends Parser { statement(); } } - setState(53); + setState(53); _errHandler.sync(this); _la = _input.LA(1); } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); @@ -170,7 +182,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_statement; } - + public StatementContext() { } public void copyFrom(StatementContext ctx) { super.copyFrom(ctx); @@ -582,7 +594,7 @@ class PainlessParser extends Parser { match(TRY); setState(119); block(); - setState(121); + setState(121); _errHandler.sync(this); _alt = 1; do { @@ -598,7 +610,7 @@ class PainlessParser extends Parser { default: throw new NoViableAltException(this); } - setState(123); + setState(123); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,12,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); @@ -658,7 +670,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_block; } - + public BlockContext() { } public void copyFrom(BlockContext ctx) { super.copyFrom(ctx); @@ -705,7 +717,7 @@ class PainlessParser extends Parser { { setState(136); match(LBRACK); - setState(138); + setState(138); _errHandler.sync(this); _la = _input.LA(1); do { @@ -715,7 +727,7 @@ class PainlessParser extends Parser { statement(); } } - setState(140); + setState(140); _errHandler.sync(this); _la = _input.LA(1); } while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR) | (1L << OCTAL))) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & ((1L << (HEX - 64)) | (1L << (INTEGER - 64)) | (1L << (DECIMAL - 64)) | (1L << (STRING - 64)) | (1L << (CHAR - 64)) | (1L << (TRUE - 64)) | (1L << (FALSE - 64)) | (1L << (NULL - 64)) | (1L << (TYPE - 64)) | (1L << (ID - 64)))) != 0) ); @@ -1196,7 +1208,7 @@ class PainlessParser extends Parser { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_expression; } - + public ExpressionContext() { } public void copyFrom(ExpressionContext ctx) { super.copyFrom(ctx); @@ -1775,7 +1787,7 @@ class PainlessParser extends Parser { } break; } - } + } } setState(262); _errHandler.sync(this); @@ -2509,7 +2521,7 @@ class PainlessParser extends Parser { case LBRACE: { { - setState(338); + setState(338); _errHandler.sync(this); _alt = 1; do { @@ -2529,7 +2541,7 @@ class PainlessParser extends Parser { default: throw new NoViableAltException(this); } - setState(340); + setState(340); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,37,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); From e7fc98a33f141e64e03fa17a645fb9672c245597 Mon Sep 17 00:00:00 2001 From: Greg Marzouka Date: Tue, 2 Feb 2016 15:32:14 -0500 Subject: [PATCH 42/49] Remove detect_noop from REST spec Unless this should be supported as a query string parameter instead, right now it only works when specified in the body. --- .../src/main/resources/rest-api-spec/api/update.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/update.json b/rest-api-spec/src/main/resources/rest-api-spec/api/update.json index 37a04cbae28..20fc3524283 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/update.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/update.json @@ -82,10 +82,6 @@ "type": "enum", "options": ["internal", "force"], "description": "Specific version type" - }, - "detect_noop": { - "type": "boolean", - "description": "Specifying as true will cause Elasticsearch to check if there are changes and, if there aren’t, turn the update request into a noop." } } }, From 64ac037a8234769ec346be7fda6f4bc7f731ff17 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 1 Feb 2016 21:53:52 +0100 Subject: [PATCH 43/49] Added an ingest qa that tests processor real world like configurations. Renamed `ingest-with-mustache` to `smoke-test-ingest-with-all-dependencies` Also renamed `ingest-disabled` to `smoke-test-ingest-disabled` so that the name is more inline with other qa smoke test modules. --- .../ingest/grok/IngestGrokPlugin.java | 2 +- .../ingest/geoip/IngestGeoIpPlugin.java | 2 +- .../build.gradle | 0 .../smoketest/IngestDisabledIT.java | 0 .../ingest_mustache/10_ingest_disabled.yaml | 0 .../build.gradle | 2 + .../ingest/AbstractMustacheTests.java | 0 .../ingest/CombineProcessorsTests.java | 209 ++++++++++++++++++ .../ingest/IngestDocumentMustacheIT.java | 0 .../IngestMustacheRemoveProcessorIT.java | 0 .../ingest/IngestMustacheSetProcessorIT.java | 0 .../ingest/TemplateServiceIT.java | 0 .../ingest/ValueSourceMustacheIT.java | 0 .../smoketest/IngestWithMustacheIT.java | 0 .../10_pipeline_with_mustache_templates.yaml | 0 settings.gradle | 4 +- 16 files changed, 215 insertions(+), 4 deletions(-) rename qa/{ingest-disabled => smoke-test-ingest-disabled}/build.gradle (100%) rename qa/{ingest-disabled => smoke-test-ingest-disabled}/src/test/java/org/elasticsearch/smoketest/IngestDisabledIT.java (100%) rename qa/{ingest-disabled => smoke-test-ingest-disabled}/src/test/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yaml (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/build.gradle (85%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/AbstractMustacheTests.java (100%) create mode 100644 qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/CombineProcessorsTests.java rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/IngestDocumentMustacheIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/IngestMustacheRemoveProcessorIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/IngestMustacheSetProcessorIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/TemplateServiceIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/ingest/ValueSourceMustacheIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/java/org/elasticsearch/smoketest/IngestWithMustacheIT.java (100%) rename qa/{ingest-with-mustache => smoke-test-ingest-with-all-dependencies}/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml (100%) diff --git a/modules/ingest-grok/src/main/java/org/elasticsearch/ingest/grok/IngestGrokPlugin.java b/modules/ingest-grok/src/main/java/org/elasticsearch/ingest/grok/IngestGrokPlugin.java index 54800ac1603..9ccccadbff3 100644 --- a/modules/ingest-grok/src/main/java/org/elasticsearch/ingest/grok/IngestGrokPlugin.java +++ b/modules/ingest-grok/src/main/java/org/elasticsearch/ingest/grok/IngestGrokPlugin.java @@ -59,7 +59,7 @@ public class IngestGrokPlugin extends Plugin { nodeModule.registerProcessor(GrokProcessor.TYPE, (templateService) -> new GrokProcessor.Factory(builtinPatterns)); } - static Map loadBuiltinPatterns() throws IOException { + public static Map loadBuiltinPatterns() throws IOException { Map builtinPatterns = new HashMap<>(); for (String pattern : PATTERN_NAMES) { try(InputStream is = IngestGrokPlugin.class.getResourceAsStream("/patterns/" + pattern)) { diff --git a/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/IngestGeoIpPlugin.java b/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/IngestGeoIpPlugin.java index f92cb7b479f..570b1e2d18f 100644 --- a/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/IngestGeoIpPlugin.java +++ b/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/IngestGeoIpPlugin.java @@ -53,7 +53,7 @@ public class IngestGeoIpPlugin extends Plugin { nodeModule.registerProcessor(GeoIpProcessor.TYPE, (templateService) -> new GeoIpProcessor.Factory(databaseReaders)); } - static Map loadDatabaseReaders(Path geoIpConfigDirectory) throws IOException { + public static Map loadDatabaseReaders(Path geoIpConfigDirectory) throws IOException { if (Files.exists(geoIpConfigDirectory) == false && Files.isDirectory(geoIpConfigDirectory)) { throw new IllegalStateException("the geoip directory [" + geoIpConfigDirectory + "] containing databases doesn't exist"); } diff --git a/qa/ingest-disabled/build.gradle b/qa/smoke-test-ingest-disabled/build.gradle similarity index 100% rename from qa/ingest-disabled/build.gradle rename to qa/smoke-test-ingest-disabled/build.gradle diff --git a/qa/ingest-disabled/src/test/java/org/elasticsearch/smoketest/IngestDisabledIT.java b/qa/smoke-test-ingest-disabled/src/test/java/org/elasticsearch/smoketest/IngestDisabledIT.java similarity index 100% rename from qa/ingest-disabled/src/test/java/org/elasticsearch/smoketest/IngestDisabledIT.java rename to qa/smoke-test-ingest-disabled/src/test/java/org/elasticsearch/smoketest/IngestDisabledIT.java diff --git a/qa/ingest-disabled/src/test/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yaml b/qa/smoke-test-ingest-disabled/src/test/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yaml similarity index 100% rename from qa/ingest-disabled/src/test/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yaml rename to qa/smoke-test-ingest-disabled/src/test/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yaml diff --git a/qa/ingest-with-mustache/build.gradle b/qa/smoke-test-ingest-with-all-dependencies/build.gradle similarity index 85% rename from qa/ingest-with-mustache/build.gradle rename to qa/smoke-test-ingest-with-all-dependencies/build.gradle index e5ca482d85a..118e36db012 100644 --- a/qa/ingest-with-mustache/build.gradle +++ b/qa/smoke-test-ingest-with-all-dependencies/build.gradle @@ -20,5 +20,7 @@ apply plugin: 'elasticsearch.rest-test' dependencies { + testCompile project(path: ':modules:ingest-grok', configuration: 'runtime') + testCompile project(path: ':plugins:ingest-geoip', configuration: 'runtime') testCompile project(path: ':modules:lang-mustache', configuration: 'runtime') } diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/AbstractMustacheTests.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/AbstractMustacheTests.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/AbstractMustacheTests.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/AbstractMustacheTests.java diff --git a/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/CombineProcessorsTests.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/CombineProcessorsTests.java new file mode 100644 index 00000000000..0245233a159 --- /dev/null +++ b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/CombineProcessorsTests.java @@ -0,0 +1,209 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest; + +import com.maxmind.geoip2.DatabaseReader; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.HppcMaps; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.ingest.core.CompoundProcessor; +import org.elasticsearch.ingest.core.IngestDocument; +import org.elasticsearch.ingest.core.Pipeline; +import org.elasticsearch.ingest.core.Processor; +import org.elasticsearch.ingest.geoip.GeoIpProcessor; +import org.elasticsearch.ingest.geoip.IngestGeoIpPlugin; +import org.elasticsearch.ingest.grok.GrokProcessor; +import org.elasticsearch.ingest.grok.IngestGrokPlugin; +import org.elasticsearch.ingest.processor.AppendProcessor; +import org.elasticsearch.ingest.processor.ConvertProcessor; +import org.elasticsearch.ingest.processor.DateProcessor; +import org.elasticsearch.ingest.processor.LowercaseProcessor; +import org.elasticsearch.ingest.processor.RemoveProcessor; +import org.elasticsearch.ingest.processor.RenameProcessor; +import org.elasticsearch.ingest.processor.SplitProcessor; +import org.elasticsearch.ingest.processor.TrimProcessor; +import org.elasticsearch.ingest.processor.UppercaseProcessor; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.StreamsUtils; + +import java.io.ByteArrayInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class CombineProcessorsTests extends ESTestCase { + + private static final String LOG = "70.193.17.92 - - [08/Sep/2014:02:54:42 +0000] \"GET /presentations/logstash-scale11x/images/ahhh___rage_face_by_samusmmx-d5g5zap.png HTTP/1.1\" 200 175208 \"http://mobile.rivals.com/board_posts.asp?SID=880&mid=198829575&fid=2208&tid=198829575&Team=&TeamId=&SiteId=\" \"Mozilla/5.0 (Linux; Android 4.2.2; VS980 4G Build/JDQ39B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.135 Mobile Safari/537.36\""; + + public void testLogging() throws Exception { + Path configDir = createTempDir(); + Path geoIpConfigDir = configDir.resolve("ingest-geoip"); + Files.createDirectories(geoIpConfigDir); + Files.copy(new ByteArrayInputStream(StreamsUtils.copyToBytesFromClasspath("/GeoLite2-City.mmdb")), geoIpConfigDir.resolve("GeoLite2-City.mmdb")); + Map databaseReaders = IngestGeoIpPlugin.loadDatabaseReaders(geoIpConfigDir); + + Map config = new HashMap<>(); + config.put("field", "log"); + config.put("pattern", "%{COMBINEDAPACHELOG}"); + Processor processor1 = new GrokProcessor.Factory(IngestGrokPlugin.loadBuiltinPatterns()).doCreate(null, config); + config = new HashMap<>(); + config.put("field", "response"); + config.put("type", "integer"); + Processor processor2 = new ConvertProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("field", "bytes"); + config.put("type", "integer"); + Processor processor3 = new ConvertProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("match_field", "timestamp"); + config.put("target_field", "timestamp"); + config.put("match_formats", Arrays.asList("dd/MMM/YYYY:HH:mm:ss Z")); + Processor processor4 = new DateProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("source_field", "clientip"); + Processor processor5 = new GeoIpProcessor.Factory(databaseReaders).create(config); + + Pipeline pipeline = new Pipeline("_id", "_description", new CompoundProcessor(processor1, processor2, processor3, processor4, processor5)); + + Map source = new HashMap<>(); + source.put("log", LOG); + IngestDocument document = new IngestDocument("_index", "_type", "_id", null, null, null, null, source); + pipeline.execute(document); + + assertThat(document.getSourceAndMetadata().size(), equalTo(17)); + assertThat(document.getSourceAndMetadata().get("request"), equalTo("/presentations/logstash-scale11x/images/ahhh___rage_face_by_samusmmx-d5g5zap.png")); + assertThat(document.getSourceAndMetadata().get("agent"), equalTo("\"Mozilla/5.0 (Linux; Android 4.2.2; VS980 4G Build/JDQ39B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.135 Mobile Safari/537.36\"")); + assertThat(document.getSourceAndMetadata().get("auth"), equalTo("-")); + assertThat(document.getSourceAndMetadata().get("ident"), equalTo("-")); + assertThat(document.getSourceAndMetadata().get("verb"), equalTo("GET")); + assertThat(document.getSourceAndMetadata().get("referrer"), equalTo("\"http://mobile.rivals.com/board_posts.asp?SID=880&mid=198829575&fid=2208&tid=198829575&Team=&TeamId=&SiteId=\"")); + assertThat(document.getSourceAndMetadata().get("response"), equalTo(200)); + assertThat(document.getSourceAndMetadata().get("bytes"), equalTo(175208)); + assertThat(document.getSourceAndMetadata().get("clientip"), equalTo("70.193.17.92")); + assertThat(document.getSourceAndMetadata().get("httpversion"), equalTo("1.1")); + assertThat(document.getSourceAndMetadata().get("rawrequest"), nullValue()); + assertThat(document.getSourceAndMetadata().get("timestamp"), equalTo("2014-09-08T02:54:42.000Z")); + Map geoInfo = (Map) document.getSourceAndMetadata().get("geoip"); + assertThat(geoInfo.size(), equalTo(5)); + assertThat(geoInfo.get("continent_name"), equalTo("North America")); + assertThat(geoInfo.get("city_name"), equalTo("Charlotte")); + assertThat(geoInfo.get("country_iso_code"), equalTo("US")); + assertThat(geoInfo.get("region_name"), equalTo("North Carolina")); + assertThat(geoInfo.get("location"), notNullValue()); + } + + private static final String PERSON = "{\n" + + " \"age\": 33,\n" + + " \"eyeColor\": \"brown\",\n" + + " \"name\": \"Miranda Goodwin\",\n" + + " \"gender\": \"male\",\n" + + " \"company\": \"ATGEN\",\n" + + " \"email\": \"mirandagoodwin@atgen.com\",\n" + + " \"phone\": \"+1 (914) 489-3656\",\n" + + " \"address\": \"713 Bartlett Place, Accoville, Puerto Rico, 9221\",\n" + + " \"registered\": \"2014-11-23T08:34:21 -01:00\",\n" + + " \"tags\": [\n" + + " \"ex\",\n" + + " \"do\",\n" + + " \"occaecat\",\n" + + " \"reprehenderit\",\n" + + " \"anim\",\n" + + " \"laboris\",\n" + + " \"cillum\"\n" + + " ],\n" + + " \"friends\": [\n" + + " {\n" + + " \"id\": 0,\n" + + " \"name\": \"Wendi Odonnell\"\n" + + " },\n" + + " {\n" + + " \"id\": 1,\n" + + " \"name\": \"Mayra Boyd\"\n" + + " },\n" + + " {\n" + + " \"id\": 2,\n" + + " \"name\": \"Lee Gonzalez\"\n" + + " }\n" + + " ]\n" + + " }"; + + @SuppressWarnings("unchecked") + public void testMutate() throws Exception { + Map config = new HashMap<>(); + // TODO: when we add foreach processor we should delete all friends.id fields + config.put("field", "friends.0.id"); + RemoveProcessor processor1 = new RemoveProcessor.Factory(TestTemplateService.instance()).create(config); + config = new HashMap<>(); + config.put("field", "tags"); + config.put("value", "new_value"); + AppendProcessor processor2 = new AppendProcessor.Factory(TestTemplateService.instance()).create(config); + config = new HashMap<>(); + config.put("field", "address"); + config.put("separator", ","); + SplitProcessor processor3 = new SplitProcessor.Factory().create(config); + config = new HashMap<>(); + // TODO: when we add foreach processor, then change the test to trim all address values + config.put("field", "address.1"); + TrimProcessor processor4 = new TrimProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("field", "company"); + LowercaseProcessor processor5 = new LowercaseProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("field", "gender"); + UppercaseProcessor processor6 = new UppercaseProcessor.Factory().create(config); + config = new HashMap<>(); + config.put("field", "eyeColor"); + config.put("to", "eye_color"); + RenameProcessor processor7 = new RenameProcessor.Factory().create(config); + Pipeline pipeline = new Pipeline("_id", "_description", new CompoundProcessor( + processor1, processor2, processor3, processor4, processor5, processor6, processor7 + )); + + Map source = XContentHelper.createParser(new BytesArray(PERSON)).map(); + IngestDocument document = new IngestDocument("_index", "_type", "_id", null, null, null, null, source); + pipeline.execute(document); + + assertThat(((List>) document.getSourceAndMetadata().get("friends")).get(0).get("id"), nullValue()); + assertThat(((List>) document.getSourceAndMetadata().get("friends")).get(1).get("id"), equalTo(1)); + assertThat(((List>) document.getSourceAndMetadata().get("friends")).get(2).get("id"), equalTo(2)); + assertThat(document.getFieldValue("tags.7", String.class), equalTo("new_value")); + + List addressDetails = document.getFieldValue("address", List.class); + assertThat(addressDetails.size(), equalTo(4)); + assertThat(addressDetails.get(0), equalTo("713 Bartlett Place")); + assertThat(addressDetails.get(1), equalTo("Accoville")); + assertThat(addressDetails.get(2), equalTo(" Puerto Rico")); + assertThat(addressDetails.get(3), equalTo(" 9221")); + + assertThat(document.getSourceAndMetadata().get("company"), equalTo("atgen")); + assertThat(document.getSourceAndMetadata().get("gender"), equalTo("MALE")); + assertThat(document.getSourceAndMetadata().get("eye_color"), equalTo("brown")); + } + +} diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestDocumentMustacheIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestDocumentMustacheIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestDocumentMustacheIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestDocumentMustacheIT.java diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestMustacheRemoveProcessorIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestMustacheRemoveProcessorIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestMustacheRemoveProcessorIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestMustacheRemoveProcessorIT.java diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestMustacheSetProcessorIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestMustacheSetProcessorIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/IngestMustacheSetProcessorIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/IngestMustacheSetProcessorIT.java diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/TemplateServiceIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/TemplateServiceIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/TemplateServiceIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/TemplateServiceIT.java diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/ValueSourceMustacheIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/ValueSourceMustacheIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/ingest/ValueSourceMustacheIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/ingest/ValueSourceMustacheIT.java diff --git a/qa/ingest-with-mustache/src/test/java/org/elasticsearch/smoketest/IngestWithMustacheIT.java b/qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/smoketest/IngestWithMustacheIT.java similarity index 100% rename from qa/ingest-with-mustache/src/test/java/org/elasticsearch/smoketest/IngestWithMustacheIT.java rename to qa/smoke-test-ingest-with-all-dependencies/src/test/java/org/elasticsearch/smoketest/IngestWithMustacheIT.java diff --git a/qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml b/qa/smoke-test-ingest-with-all-dependencies/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml similarity index 100% rename from qa/ingest-with-mustache/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml rename to qa/smoke-test-ingest-with-all-dependencies/src/test/resources/rest-api-spec/test/ingest_mustache/10_pipeline_with_mustache_templates.yaml diff --git a/settings.gradle b/settings.gradle index f7a7fc71772..df2ce16c8bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,8 +40,8 @@ List projects = [ 'qa:smoke-test-client', 'qa:smoke-test-multinode', 'qa:smoke-test-plugins', - 'qa:ingest-with-mustache', - 'qa:ingest-disabled', + 'qa:smoke-test-ingest-with-all-dependencies', + 'qa:smoke-test-ingest-disabled', 'qa:vagrant', ] From 0a1580eefaf1708624c92ab29bd01c8f0d8a4218 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 1 Feb 2016 13:40:54 -0800 Subject: [PATCH 44/49] revert PipelineFactoryError handling with throwing ElasticsearchParseException in ingest pipeline creation --- .../ingest/SimulatePipelineResponse.java | 78 +++++---------- .../SimulatePipelineTransportAction.java | 6 +- .../action/ingest/WritePipelineResponse.java | 21 ---- .../WritePipelineResponseRestListener.java | 41 -------- .../elasticsearch/ingest/PipelineStore.java | 26 ++--- .../ingest/core/ConfigurationUtils.java | 54 +++++++---- .../elasticsearch/ingest/core/Pipeline.java | 23 ++--- .../ingest/core/PipelineFactoryError.java | 96 ------------------- .../ingest/core/PipelineFactoryResult.java | 43 --------- .../elasticsearch/ingest/core/Processor.java | 3 - .../ConfigurationPropertyException.java | 53 ---------- .../ingest/processor/ConvertProcessor.java | 10 +- .../action/ingest/RestPutPipelineAction.java | 7 +- .../ingest/RestSimulatePipelineAction.java | 2 +- .../ingest/WritePipelineResponseTests.java | 10 +- .../elasticsearch/ingest/IngestClientIT.java | 29 ++---- .../ingest/PipelineStoreTests.java | 51 +++------- .../ingest/core/ConfigurationUtilsTests.java | 4 +- .../ingest/core/PipelineFactoryTests.java | 6 +- .../AppendProcessorFactoryTests.java | 8 +- .../ConvertProcessorFactoryTests.java | 13 ++- .../processor/DateProcessorFactoryTests.java | 7 +- .../processor/FailProcessorFactoryTests.java | 3 +- .../processor/GsubProcessorFactoryTests.java | 7 +- .../processor/JoinProcessorFactoryTests.java | 5 +- .../LowercaseProcessorFactoryTests.java | 3 +- .../RemoveProcessorFactoryTests.java | 3 +- .../RenameProcessorFactoryTests.java | 5 +- .../processor/SetProcessorFactoryTests.java | 7 +- .../processor/SplitProcessorFactoryTests.java | 5 +- .../processor/TrimProcessorFactoryTests.java | 3 +- .../UppercaseProcessorFactoryTests.java | 3 +- .../grok/GrokProcessorFactoryTests.java | 7 +- .../ingest/geoip/GeoIpProcessor.java | 10 +- .../geoip/GeoIpProcessorFactoryTests.java | 9 +- .../rest-api-spec/test/ingest/10_crud.yaml | 12 +-- .../test/ingest/40_simulate.yaml | 27 +++--- 37 files changed, 190 insertions(+), 510 deletions(-) delete mode 100644 core/src/main/java/org/elasticsearch/action/ingest/WritePipelineResponseRestListener.java delete mode 100644 core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryError.java delete mode 100644 core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryResult.java delete mode 100644 core/src/main/java/org/elasticsearch/ingest/processor/ConfigurationPropertyException.java diff --git a/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineResponse.java b/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineResponse.java index 4337d0ee165..c7c0822f04a 100644 --- a/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineResponse.java +++ b/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineResponse.java @@ -22,31 +22,24 @@ package org.elasticsearch.action.ingest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.StatusToXContent; +import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilderString; -import org.elasticsearch.ingest.core.PipelineFactoryError; -import org.elasticsearch.rest.RestStatus; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class SimulatePipelineResponse extends ActionResponse implements StatusToXContent { +public class SimulatePipelineResponse extends ActionResponse implements ToXContent { private String pipelineId; private boolean verbose; private List results; - private PipelineFactoryError error; public SimulatePipelineResponse() { } - public SimulatePipelineResponse(PipelineFactoryError error) { - this.error = error; - } - public SimulatePipelineResponse(String pipelineId, boolean verbose, List responses) { this.pipelineId = pipelineId; this.verbose = verbose; @@ -65,69 +58,42 @@ public class SimulatePipelineResponse extends ActionResponse implements StatusTo return verbose; } - public boolean isError() { - return error != null; - } - - @Override - public RestStatus status() { - if (isError()) { - return RestStatus.BAD_REQUEST; - } - return RestStatus.OK; - } - @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeBoolean(isError()); - if (isError()) { - error.writeTo(out); - } else { - out.writeString(pipelineId); - out.writeBoolean(verbose); - out.writeVInt(results.size()); - for (SimulateDocumentResult response : results) { - response.writeTo(out); - } + out.writeString(pipelineId); + out.writeBoolean(verbose); + out.writeVInt(results.size()); + for (SimulateDocumentResult response : results) { + response.writeTo(out); } } @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - boolean isError = in.readBoolean(); - if (isError) { - error = new PipelineFactoryError(); - error.readFrom(in); - } else { - this.pipelineId = in.readString(); - boolean verbose = in.readBoolean(); - int responsesLength = in.readVInt(); - results = new ArrayList<>(); - for (int i = 0; i < responsesLength; i++) { - SimulateDocumentResult simulateDocumentResult; - if (verbose) { - simulateDocumentResult = SimulateDocumentVerboseResult.readSimulateDocumentVerboseResultFrom(in); - } else { - simulateDocumentResult = SimulateDocumentBaseResult.readSimulateDocumentSimpleResult(in); - } - results.add(simulateDocumentResult); + this.pipelineId = in.readString(); + boolean verbose = in.readBoolean(); + int responsesLength = in.readVInt(); + results = new ArrayList<>(); + for (int i = 0; i < responsesLength; i++) { + SimulateDocumentResult simulateDocumentResult; + if (verbose) { + simulateDocumentResult = SimulateDocumentVerboseResult.readSimulateDocumentVerboseResultFrom(in); + } else { + simulateDocumentResult = SimulateDocumentBaseResult.readSimulateDocumentSimpleResult(in); } + results.add(simulateDocumentResult); } } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (isError()) { - error.toXContent(builder, params); - } else { - builder.startArray(Fields.DOCUMENTS); - for (SimulateDocumentResult response : results) { - response.toXContent(builder, params); - } - builder.endArray(); + builder.startArray(Fields.DOCUMENTS); + for (SimulateDocumentResult response : results) { + response.toXContent(builder, params); } + builder.endArray(); return builder; } diff --git a/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java b/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java index 3d6586315ad..4f9a219c8ad 100644 --- a/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java +++ b/core/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java @@ -19,6 +19,7 @@ package org.elasticsearch.action.ingest; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; @@ -27,8 +28,6 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.ingest.PipelineStore; -import org.elasticsearch.ingest.core.PipelineFactoryError; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import org.elasticsearch.node.service.NodeService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -58,9 +57,6 @@ public class SimulatePipelineTransportAction extends HandledTransportAction { - - public WritePipelineResponseRestListener(RestChannel channel) { - super(channel); - } - - @Override - protected void addCustomFields(XContentBuilder builder, WritePipelineResponse response) throws IOException { - if (!response.isAcknowledged()) { - response.getError().toXContent(builder, null); - } - } -} - diff --git a/core/src/main/java/org/elasticsearch/ingest/PipelineStore.java b/core/src/main/java/org/elasticsearch/ingest/PipelineStore.java index 21128a94b65..e2d68199f43 100644 --- a/core/src/main/java/org/elasticsearch/ingest/PipelineStore.java +++ b/core/src/main/java/org/elasticsearch/ingest/PipelineStore.java @@ -20,6 +20,7 @@ package org.elasticsearch.ingest; import org.apache.lucene.util.IOUtils; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ingest.DeletePipelineRequest; @@ -36,10 +37,8 @@ import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.ingest.core.Pipeline; -import org.elasticsearch.ingest.core.PipelineFactoryError; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.ingest.core.TemplateService; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import org.elasticsearch.script.ScriptService; import java.io.Closeable; @@ -104,8 +103,10 @@ public class PipelineStore extends AbstractComponent implements Closeable, Clust for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { try { pipelines.put(pipeline.getId(), factory.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactoryRegistry)); + } catch (ElasticsearchParseException e) { + throw e; } catch (Exception e) { - throw new RuntimeException(e); + throw new ElasticsearchParseException("Error updating pipeline with id [" + pipeline.getId() + "]", e); } } this.pipelines = Collections.unmodifiableMap(pipelines); @@ -154,9 +155,10 @@ public class PipelineStore extends AbstractComponent implements Closeable, Clust public void put(ClusterService clusterService, PutPipelineRequest request, ActionListener listener) { // validates the pipeline and processor configuration before submitting a cluster update task: Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false).v2(); - WritePipelineResponse response = validatePipelineResponse(request.getId(), pipelineConfig); - if (response != null) { - listener.onResponse(response); + try { + factory.create(request.getId(), pipelineConfig, processorFactoryRegistry); + } catch(Exception e) { + listener.onFailure(e); return; } clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), new AckedClusterStateUpdateTask(request, listener) { @@ -234,16 +236,4 @@ public class PipelineStore extends AbstractComponent implements Closeable, Clust } return result; } - - WritePipelineResponse validatePipelineResponse(String id, Map config) { - try { - factory.create(id, config, processorFactoryRegistry); - return null; - } catch (ConfigurationPropertyException e) { - return new WritePipelineResponse(new PipelineFactoryError(e)); - } catch (Exception e) { - return new WritePipelineResponse(new PipelineFactoryError(e.getMessage())); - } - } - } diff --git a/core/src/main/java/org/elasticsearch/ingest/core/ConfigurationUtils.java b/core/src/main/java/org/elasticsearch/ingest/core/ConfigurationUtils.java index 69adc0f9492..bd3fd8cfb6e 100644 --- a/core/src/main/java/org/elasticsearch/ingest/core/ConfigurationUtils.java +++ b/core/src/main/java/org/elasticsearch/ingest/core/ConfigurationUtils.java @@ -19,7 +19,8 @@ package org.elasticsearch.ingest.core; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; import java.util.List; import java.util.Map; @@ -32,7 +33,7 @@ public final class ConfigurationUtils { /** * Returns and removes the specified optional property from the specified configuration map. * - * If the property value isn't of type string a {@link ConfigurationPropertyException} is thrown. + * If the property value isn't of type string a {@link ElasticsearchParseException} is thrown. */ public static String readOptionalStringProperty(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); @@ -42,8 +43,8 @@ public final class ConfigurationUtils { /** * Returns and removes the specified property from the specified configuration map. * - * If the property value isn't of type string an {@link ConfigurationPropertyException} is thrown. - * If the property is missing an {@link ConfigurationPropertyException} is thrown + * If the property value isn't of type string an {@link ElasticsearchParseException} is thrown. + * If the property is missing an {@link ElasticsearchParseException} is thrown */ public static String readStringProperty(String processorType, String processorTag, Map configuration, String propertyName) { return readStringProperty(processorType, processorTag, configuration, propertyName, null); @@ -52,15 +53,15 @@ public final class ConfigurationUtils { /** * Returns and removes the specified property from the specified configuration map. * - * If the property value isn't of type string a {@link ConfigurationPropertyException} is thrown. - * If the property is missing and no default value has been specified a {@link ConfigurationPropertyException} is thrown + * If the property value isn't of type string a {@link ElasticsearchParseException} is thrown. + * If the property is missing and no default value has been specified a {@link ElasticsearchParseException} is thrown */ public static String readStringProperty(String processorType, String processorTag, Map configuration, String propertyName, String defaultValue) { Object value = configuration.remove(propertyName); if (value == null && defaultValue != null) { return defaultValue; } else if (value == null) { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "required property is missing"); + throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing"); } return readString(processorType, processorTag, propertyName, value); } @@ -72,13 +73,13 @@ public final class ConfigurationUtils { if (value instanceof String) { return (String) value; } - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "property isn't a string, but of type [" + value.getClass().getName() + "]"); + throw newConfigurationException(processorType, processorTag, propertyName, "property isn't a string, but of type [" + value.getClass().getName() + "]"); } /** * Returns and removes the specified property of type list from the specified configuration map. * - * If the property value isn't of type list an {@link ConfigurationPropertyException} is thrown. + * If the property value isn't of type list an {@link ElasticsearchParseException} is thrown. */ public static List readOptionalList(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); @@ -91,13 +92,13 @@ public final class ConfigurationUtils { /** * Returns and removes the specified property of type list from the specified configuration map. * - * If the property value isn't of type list an {@link ConfigurationPropertyException} is thrown. - * If the property is missing an {@link ConfigurationPropertyException} is thrown + * If the property value isn't of type list an {@link ElasticsearchParseException} is thrown. + * If the property is missing an {@link ElasticsearchParseException} is thrown */ public static List readList(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); if (value == null) { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "required property is missing"); + throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing"); } return readList(processorType, processorTag, propertyName, value); @@ -109,20 +110,20 @@ public final class ConfigurationUtils { List stringList = (List) value; return stringList; } else { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "property isn't a list, but of type [" + value.getClass().getName() + "]"); + throw newConfigurationException(processorType, processorTag, propertyName, "property isn't a list, but of type [" + value.getClass().getName() + "]"); } } /** * Returns and removes the specified property of type map from the specified configuration map. * - * If the property value isn't of type map an {@link ConfigurationPropertyException} is thrown. - * If the property is missing an {@link ConfigurationPropertyException} is thrown + * If the property value isn't of type map an {@link ElasticsearchParseException} is thrown. + * If the property is missing an {@link ElasticsearchParseException} is thrown */ public static Map readMap(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); if (value == null) { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "required property is missing"); + throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing"); } return readMap(processorType, processorTag, propertyName, value); @@ -131,7 +132,7 @@ public final class ConfigurationUtils { /** * Returns and removes the specified property of type map from the specified configuration map. * - * If the property value isn't of type map an {@link ConfigurationPropertyException} is thrown. + * If the property value isn't of type map an {@link ElasticsearchParseException} is thrown. */ public static Map readOptionalMap(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); @@ -148,7 +149,7 @@ public final class ConfigurationUtils { Map map = (Map) value; return map; } else { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "property isn't a map, but of type [" + value.getClass().getName() + "]"); + throw newConfigurationException(processorType, processorTag, propertyName, "property isn't a map, but of type [" + value.getClass().getName() + "]"); } } @@ -158,8 +159,23 @@ public final class ConfigurationUtils { public static Object readObject(String processorType, String processorTag, Map configuration, String propertyName) { Object value = configuration.remove(propertyName); if (value == null) { - throw new ConfigurationPropertyException(processorType, processorTag, propertyName, "required property is missing"); + throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing"); } return value; } + + public static ElasticsearchParseException newConfigurationException(String processorType, String processorTag, String propertyName, String reason) { + ElasticsearchParseException exception = new ElasticsearchParseException("[" + propertyName + "] " + reason); + + if (processorType != null) { + exception.addHeader("processor_type", processorType); + } + if (processorTag != null) { + exception.addHeader("processor_tag", processorTag); + } + if (propertyName != null) { + exception.addHeader("property_name", propertyName); + } + return exception; + } } diff --git a/core/src/main/java/org/elasticsearch/ingest/core/Pipeline.java b/core/src/main/java/org/elasticsearch/ingest/core/Pipeline.java index 5c654fbce21..1c560fa6bcc 100644 --- a/core/src/main/java/org/elasticsearch/ingest/core/Pipeline.java +++ b/core/src/main/java/org/elasticsearch/ingest/core/Pipeline.java @@ -19,7 +19,7 @@ package org.elasticsearch.ingest.core; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; +import org.elasticsearch.ElasticsearchParseException; import java.util.ArrayList; import java.util.Arrays; @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; + /** * A pipeline is a list of {@link Processor} instances grouped under a unique id. */ @@ -84,20 +85,20 @@ public final class Pipeline { public final static class Factory { - public Pipeline create(String id, Map config, Map processorRegistry) throws ConfigurationPropertyException { + public Pipeline create(String id, Map config, Map processorRegistry) throws Exception { String description = ConfigurationUtils.readOptionalStringProperty(null, null, config, DESCRIPTION_KEY); List>> processorConfigs = ConfigurationUtils.readList(null, null, config, PROCESSORS_KEY); List processors = readProcessorConfigs(processorConfigs, processorRegistry); List>> onFailureProcessorConfigs = ConfigurationUtils.readOptionalList(null, null, config, ON_FAILURE_KEY); List onFailureProcessors = readProcessorConfigs(onFailureProcessorConfigs, processorRegistry); if (config.isEmpty() == false) { - throw new ConfigurationPropertyException("pipeline [" + id + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); + throw new ElasticsearchParseException("pipeline [" + id + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); } CompoundProcessor compoundProcessor = new CompoundProcessor(Collections.unmodifiableList(processors), Collections.unmodifiableList(onFailureProcessors)); return new Pipeline(id, description, compoundProcessor); } - private List readProcessorConfigs(List>> processorConfigs, Map processorRegistry) throws ConfigurationPropertyException { + private List readProcessorConfigs(List>> processorConfigs, Map processorRegistry) throws Exception { List processors = new ArrayList<>(); if (processorConfigs != null) { for (Map> processorConfigWithKey : processorConfigs) { @@ -110,28 +111,22 @@ public final class Pipeline { return processors; } - private Processor readProcessor(Map processorRegistry, String type, Map config) throws ConfigurationPropertyException { + private Processor readProcessor(Map processorRegistry, String type, Map config) throws Exception { Processor.Factory factory = processorRegistry.get(type); if (factory != null) { List>> onFailureProcessorConfigs = ConfigurationUtils.readOptionalList(null, null, config, ON_FAILURE_KEY); List onFailureProcessors = readProcessorConfigs(onFailureProcessorConfigs, processorRegistry); Processor processor; - try { - processor = factory.create(config); - } catch (ConfigurationPropertyException e) { - throw e; - } catch (Exception e) { - throw new ConfigurationPropertyException(e.getMessage()); - } + processor = factory.create(config); if (!config.isEmpty()) { - throw new ConfigurationPropertyException("processor [" + type + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); + throw new ElasticsearchParseException("processor [" + type + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); } if (onFailureProcessors.isEmpty()) { return processor; } return new CompoundProcessor(Collections.singletonList(processor), onFailureProcessors); } - throw new ConfigurationPropertyException("No processor type exists with name [" + type + "]"); + throw new ElasticsearchParseException("No processor type exists with name [" + type + "]"); } } } diff --git a/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryError.java b/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryError.java deleted file mode 100644 index b987e1ee266..00000000000 --- a/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryError.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.core; - - -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.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentBuilderString; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; - -import java.io.IOException; - -public class PipelineFactoryError implements Streamable, ToXContent { - private String reason; - private String processorType; - private String processorTag; - private String processorPropertyName; - - public PipelineFactoryError() { - - } - - public PipelineFactoryError(ConfigurationPropertyException e) { - this.reason = e.getMessage(); - this.processorType = e.getProcessorType(); - this.processorTag = e.getProcessorTag(); - this.processorPropertyName = e.getPropertyName(); - } - - public PipelineFactoryError(String reason) { - this.reason = "Constructing Pipeline failed:" + reason; - } - - public String getReason() { - return reason; - } - - public String getProcessorTag() { - return processorTag; - } - - public String getProcessorPropertyName() { - return processorPropertyName; - } - - public String getProcessorType() { - return processorType; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - reason = in.readString(); - processorType = in.readOptionalString(); - processorTag = in.readOptionalString(); - processorPropertyName = in.readOptionalString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(reason); - out.writeOptionalString(processorType); - out.writeOptionalString(processorTag); - out.writeOptionalString(processorPropertyName); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("error"); - builder.field("type", processorType); - builder.field("tag", processorTag); - builder.field("reason", reason); - builder.field("property_name", processorPropertyName); - builder.endObject(); - return builder; - } -} diff --git a/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryResult.java b/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryResult.java deleted file mode 100644 index ab284981b33..00000000000 --- a/core/src/main/java/org/elasticsearch/ingest/core/PipelineFactoryResult.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.core; - -public class PipelineFactoryResult { - private final Pipeline pipeline; - private final PipelineFactoryError error; - - public PipelineFactoryResult(Pipeline pipeline) { - this.pipeline = pipeline; - this.error = null; - } - - public PipelineFactoryResult(PipelineFactoryError error) { - this.error = error; - this.pipeline = null; - } - - public Pipeline getPipeline() { - return pipeline; - } - - public PipelineFactoryError getError() { - return error; - } -} diff --git a/core/src/main/java/org/elasticsearch/ingest/core/Processor.java b/core/src/main/java/org/elasticsearch/ingest/core/Processor.java index 28049983692..8cdff8714c4 100644 --- a/core/src/main/java/org/elasticsearch/ingest/core/Processor.java +++ b/core/src/main/java/org/elasticsearch/ingest/core/Processor.java @@ -17,11 +17,8 @@ * under the License. */ - package org.elasticsearch.ingest.core; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; - import java.util.Map; /** diff --git a/core/src/main/java/org/elasticsearch/ingest/processor/ConfigurationPropertyException.java b/core/src/main/java/org/elasticsearch/ingest/processor/ConfigurationPropertyException.java deleted file mode 100644 index dbc35c9334f..00000000000 --- a/core/src/main/java/org/elasticsearch/ingest/processor/ConfigurationPropertyException.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.processor; - -/** - * Exception class thrown by processor factories. - */ -public class ConfigurationPropertyException extends RuntimeException { - private String processorType; - private String processorTag; - private String propertyName; - - public ConfigurationPropertyException(String processorType, String processorTag, String propertyName, String message) { - super("[" + propertyName + "] " + message); - this.processorTag = processorTag; - this.processorType = processorType; - this.propertyName = propertyName; - } - - public ConfigurationPropertyException(String errorMessage) { - super(errorMessage); - } - - public String getPropertyName() { - return propertyName; - } - - public String getProcessorType() { - return processorType; - } - - public String getProcessorTag() { - return processorTag; - } -} - diff --git a/core/src/main/java/org/elasticsearch/ingest/processor/ConvertProcessor.java b/core/src/main/java/org/elasticsearch/ingest/processor/ConvertProcessor.java index 213e3ec2c78..7cc85909a32 100644 --- a/core/src/main/java/org/elasticsearch/ingest/processor/ConvertProcessor.java +++ b/core/src/main/java/org/elasticsearch/ingest/processor/ConvertProcessor.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessor; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.IngestDocument; @@ -29,6 +30,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import static org.elasticsearch.ingest.core.ConfigurationUtils.newConfigurationException; + /** * Processor that converts fields content to a different type. Supported types are: integer, float, boolean and string. * Throws exception if the field is not there or the conversion fails. @@ -80,11 +83,11 @@ public class ConvertProcessor extends AbstractProcessor { public abstract Object convert(Object value); - public static Type fromString(String type) { + public static Type fromString(String processorTag, String propertyName, String type) { try { return Type.valueOf(type.toUpperCase(Locale.ROOT)); } catch(IllegalArgumentException e) { - throw new IllegalArgumentException("type [" + type + "] not supported, cannot convert field.", e); + throw newConfigurationException(TYPE, processorTag, propertyName, "type [" + type + "] not supported, cannot convert field."); } } } @@ -138,7 +141,8 @@ public class ConvertProcessor extends AbstractProcessor { @Override public ConvertProcessor doCreate(String processorTag, Map config) throws Exception { String field = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "field"); - Type convertType = Type.fromString(ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "type")); + String typeProperty = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "type"); + Type convertType = Type.fromString(processorTag, "type", typeProperty); return new ConvertProcessor(processorTag, field, convertType); } } diff --git a/core/src/main/java/org/elasticsearch/rest/action/ingest/RestPutPipelineAction.java b/core/src/main/java/org/elasticsearch/rest/action/ingest/RestPutPipelineAction.java index badccbb9579..a96ed3d6424 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/ingest/RestPutPipelineAction.java +++ b/core/src/main/java/org/elasticsearch/rest/action/ingest/RestPutPipelineAction.java @@ -20,13 +20,9 @@ package org.elasticsearch.rest.action.ingest; import org.elasticsearch.action.ingest.PutPipelineRequest; -import org.elasticsearch.action.ingest.WritePipelineResponse; -import org.elasticsearch.action.ingest.WritePipelineResponseRestListener; import org.elasticsearch.client.Client; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentBuilderString; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestController; @@ -34,7 +30,6 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.support.AcknowledgedRestListener; import org.elasticsearch.rest.action.support.RestActions; -import java.io.IOException; public class RestPutPipelineAction extends BaseRestHandler { @@ -49,7 +44,7 @@ public class RestPutPipelineAction extends BaseRestHandler { PutPipelineRequest request = new PutPipelineRequest(restRequest.param("id"), RestActions.getRestContent(restRequest)); request.masterNodeTimeout(restRequest.paramAsTime("master_timeout", request.masterNodeTimeout())); request.timeout(restRequest.paramAsTime("timeout", request.timeout())); - client.admin().cluster().putPipeline(request, new WritePipelineResponseRestListener(channel)); + client.admin().cluster().putPipeline(request, new AcknowledgedRestListener<>(channel)); } } diff --git a/core/src/main/java/org/elasticsearch/rest/action/ingest/RestSimulatePipelineAction.java b/core/src/main/java/org/elasticsearch/rest/action/ingest/RestSimulatePipelineAction.java index 94f80a9b611..fc2e834ea75 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/ingest/RestSimulatePipelineAction.java +++ b/core/src/main/java/org/elasticsearch/rest/action/ingest/RestSimulatePipelineAction.java @@ -47,6 +47,6 @@ public class RestSimulatePipelineAction extends BaseRestHandler { SimulatePipelineRequest request = new SimulatePipelineRequest(RestActions.getRestContent(restRequest)); request.setId(restRequest.param("id")); request.setVerbose(restRequest.paramAsBoolean("verbose", false)); - client.admin().cluster().simulatePipeline(request, new RestStatusToXContentListener<>(channel)); + client.admin().cluster().simulatePipeline(request, new RestToXContentListener<>(channel)); } } diff --git a/core/src/test/java/org/elasticsearch/action/ingest/WritePipelineResponseTests.java b/core/src/test/java/org/elasticsearch/action/ingest/WritePipelineResponseTests.java index 8eb3f4ece75..3f252c37072 100644 --- a/core/src/test/java/org/elasticsearch/action/ingest/WritePipelineResponseTests.java +++ b/core/src/test/java/org/elasticsearch/action/ingest/WritePipelineResponseTests.java @@ -21,13 +21,11 @@ package org.elasticsearch.action.ingest; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.ingest.core.PipelineFactoryError; import org.elasticsearch.test.ESTestCase; import java.io.IOException; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.nullValue; public class WritePipelineResponseTests extends ESTestCase { @@ -45,17 +43,13 @@ public class WritePipelineResponseTests extends ESTestCase { } public void testSerializationWithError() throws IOException { - PipelineFactoryError error = new PipelineFactoryError("error"); - WritePipelineResponse response = new WritePipelineResponse(error); + WritePipelineResponse response = new WritePipelineResponse(); BytesStreamOutput out = new BytesStreamOutput(); response.writeTo(out); StreamInput streamInput = StreamInput.wrap(out.bytes()); WritePipelineResponse otherResponse = new WritePipelineResponse(); otherResponse.readFrom(streamInput); - assertThat(otherResponse.getError().getReason(), equalTo(response.getError().getReason())); - assertThat(otherResponse.getError().getProcessorType(), equalTo(response.getError().getProcessorType())); - assertThat(otherResponse.getError().getProcessorTag(), equalTo(response.getError().getProcessorTag())); - assertThat(otherResponse.getError().getProcessorPropertyName(), equalTo(response.getError().getProcessorPropertyName())); + assertThat(otherResponse.isAcknowledged(), equalTo(response.isAcknowledged())); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java b/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java index bcbe41dd66f..a2995964a0d 100644 --- a/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java +++ b/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; @@ -38,11 +39,13 @@ import org.elasticsearch.ingest.core.IngestDocument; import org.elasticsearch.node.NodeModule; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.RemoteTransportException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; @@ -201,23 +204,7 @@ public class IngestClientIT extends ESIntegTestCase { assertThat(getResponse.pipelines().size(), equalTo(0)); } - public void testPutWithPipelineError() throws Exception { - BytesReference source = jsonBuilder().startObject() - .field("description", "my_pipeline") - .startArray("processors") - .startObject() - .startObject("not_found") - .endObject() - .endObject() - .endArray() - .endObject().bytes(); - PutPipelineRequest putPipelineRequest = new PutPipelineRequest("_id", source); - WritePipelineResponse response = client().admin().cluster().putPipeline(putPipelineRequest).get(); - assertThat(response.isAcknowledged(), equalTo(false)); - assertThat(response.getError().getReason(), equalTo("No processor type exists with name [not_found]")); - } - - public void testPutWithProcessorFactoryError() throws Exception { + public void testPutWithPipelineFactoryError() throws Exception { BytesReference source = jsonBuilder().startObject() .field("description", "my_pipeline") .startArray("processors") @@ -229,9 +216,11 @@ public class IngestClientIT extends ESIntegTestCase { .endArray() .endObject().bytes(); PutPipelineRequest putPipelineRequest = new PutPipelineRequest("_id", source); - WritePipelineResponse response = client().admin().cluster().putPipeline(putPipelineRequest).get(); - assertThat(response.isAcknowledged(), equalTo(false)); - assertThat(response.getError().getReason(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); + try { + client().admin().cluster().putPipeline(putPipelineRequest).get(); + } catch (ExecutionException e) { + assertThat(e.getCause().getCause().getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); + } } @Override diff --git a/core/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java b/core/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java index a75a84f0379..bdf1f7d49c1 100644 --- a/core/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java @@ -19,10 +19,10 @@ package org.elasticsearch.ingest; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ingest.DeletePipelineRequest; import org.elasticsearch.action.ingest.PutPipelineRequest; -import org.elasticsearch.action.ingest.WritePipelineResponse; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.MetaData; @@ -103,42 +103,21 @@ public class PipelineStoreTests extends ESTestCase { } public void testPutWithErrorResponse() { + String id = "_id"; + Pipeline pipeline = store.get(id); + assertThat(pipeline, nullValue()); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - } - - public void testConstructPipelineResponseSuccess() { - Map processorConfig = new HashMap<>(); - processorConfig.put("field", "foo"); - processorConfig.put("value", "bar"); - Map pipelineConfig = new HashMap<>(); - pipelineConfig.put("description", "_description"); - pipelineConfig.put("processors", Collections.singletonList(Collections.singletonMap("set", processorConfig))); - WritePipelineResponse response = store.validatePipelineResponse("test_id", pipelineConfig); - assertThat(response, nullValue()); - } - - public void testConstructPipelineResponseMissingProcessorsFieldException() { - Map pipelineConfig = new HashMap<>(); - pipelineConfig.put("description", "_description"); - WritePipelineResponse response = store.validatePipelineResponse("test_id", pipelineConfig); - assertThat(response.getError().getProcessorType(), is(nullValue())); - assertThat(response.getError().getProcessorTag(), is(nullValue())); - assertThat(response.getError().getProcessorPropertyName(), equalTo("processors")); - assertThat(response.getError().getReason(), equalTo("[processors] required property is missing")); - } - - public void testConstructPipelineResponseConfigurationException() { - Map processorConfig = new HashMap<>(); - processorConfig.put("field", "foo"); - Map pipelineConfig = new HashMap<>(); - pipelineConfig.put("description", "_description"); - pipelineConfig.put("processors", Collections.singletonList(Collections.singletonMap("set", processorConfig))); - WritePipelineResponse response = store.validatePipelineResponse("test_id", pipelineConfig); - - assertThat(response.getError().getProcessorTag(), nullValue()); - assertThat(response.getError().getProcessorType(), equalTo("set")); - assertThat(response.getError().getProcessorPropertyName(), equalTo("value")); - assertThat(response.getError().getReason(), equalTo("[value] required property is missing")); + PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"description\": \"empty processors\"}")); + clusterState = store.innerPut(putRequest, clusterState); + try { + store.innerUpdatePipelines(clusterState); + fail("should fail"); + } catch (ElasticsearchParseException e) { + assertThat(e.getMessage(), equalTo("[processors] required property is missing")); + } + pipeline = store.get(id); + assertThat(pipeline, nullValue()); } public void testDelete() { diff --git a/core/src/test/java/org/elasticsearch/ingest/core/ConfigurationUtilsTests.java b/core/src/test/java/org/elasticsearch/ingest/core/ConfigurationUtilsTests.java index 954a03c2172..722f14e396e 100644 --- a/core/src/test/java/org/elasticsearch/ingest/core/ConfigurationUtilsTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/core/ConfigurationUtilsTests.java @@ -19,7 +19,7 @@ package org.elasticsearch.ingest.core; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.test.ESTestCase; import org.junit.Before; @@ -58,7 +58,7 @@ public class ConfigurationUtilsTests extends ESTestCase { public void testReadStringPropertyInvalidType() { try { ConfigurationUtils.readStringProperty(null, null, config, "arr"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[arr] property isn't a string, but of type [java.util.Arrays$ArrayList]")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/core/PipelineFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/core/PipelineFactoryTests.java index 746ac2f5617..04f887e9383 100644 --- a/core/src/test/java/org/elasticsearch/ingest/core/PipelineFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/core/PipelineFactoryTests.java @@ -19,8 +19,8 @@ package org.elasticsearch.ingest.core; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestProcessor; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import org.elasticsearch.test.ESTestCase; import java.util.Arrays; @@ -59,7 +59,7 @@ public class PipelineFactoryTests extends ESTestCase { try { factory.create("_id", pipelineConfig, Collections.emptyMap()); fail("should fail, missing required [processors] field"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[processors] required property is missing")); } } @@ -91,7 +91,7 @@ public class PipelineFactoryTests extends ESTestCase { Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); try { factory.create("_id", pipelineConfig, processorRegistry); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/AppendProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/AppendProcessorFactoryTests.java index c4c13a6ab7d..7350e3d9c43 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/AppendProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/AppendProcessorFactoryTests.java @@ -19,9 +19,9 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.ingest.core.AbstractProcessorFactory; -import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; import org.junit.Before; @@ -65,7 +65,7 @@ public class AppendProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -76,7 +76,7 @@ public class AppendProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[value] required property is missing")); } } @@ -88,7 +88,7 @@ public class AppendProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[value] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/ConvertProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/ConvertProcessorFactoryTests.java index a07cec5c4e7..831e87436ba 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/ConvertProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/ConvertProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -28,6 +29,7 @@ import java.util.HashMap; import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.nullValue; public class ConvertProcessorFactoryTests extends ESTestCase { @@ -54,8 +56,11 @@ public class ConvertProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage(), Matchers.equalTo("type [" + type + "] not supported, cannot convert field.")); + } catch (ElasticsearchParseException e) { + assertThat(e.getMessage(), Matchers.equalTo("[type] type [" + type + "] not supported, cannot convert field.")); + assertThat(e.getHeader("processor_type").get(0), equalTo(ConvertProcessor.TYPE)); + assertThat(e.getHeader("property_name").get(0), equalTo("type")); + assertThat(e.getHeader("processor_tag"), nullValue()); } } @@ -67,7 +72,7 @@ public class ConvertProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), Matchers.equalTo("[field] required property is missing")); } } @@ -79,7 +84,7 @@ public class ConvertProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), Matchers.equalTo("[type] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/DateProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/DateProcessorFactoryTests.java index 1139f1968f7..7ea1de17fc0 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/DateProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/DateProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -64,7 +65,7 @@ public class DateProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("processor creation should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), containsString("[match_field] required property is missing")); } } @@ -80,7 +81,7 @@ public class DateProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("processor creation should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), containsString("[match_formats] required property is missing")); } } @@ -170,7 +171,7 @@ public class DateProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("processor creation should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), containsString("[match_formats] property isn't a list, but of type [java.lang.String]")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/FailProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/FailProcessorFactoryTests.java index 661a6383dfd..0d88710c80d 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/FailProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/FailProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; @@ -55,7 +56,7 @@ public class FailProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[message] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/GsubProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/GsubProcessorFactoryTests.java index bce033091ac..2440ff68408 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/GsubProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/GsubProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -53,7 +54,7 @@ public class GsubProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -66,7 +67,7 @@ public class GsubProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[pattern] required property is missing")); } } @@ -79,7 +80,7 @@ public class GsubProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[replacement] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/JoinProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/JoinProcessorFactoryTests.java index 51eb989beda..c374b8a3318 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/JoinProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/JoinProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -50,7 +51,7 @@ public class JoinProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -62,7 +63,7 @@ public class JoinProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[separator] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/LowercaseProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/LowercaseProcessorFactoryTests.java index 32eefa07896..09d676b3b30 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/LowercaseProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/LowercaseProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -47,7 +48,7 @@ public class LowercaseProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/RemoveProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/RemoveProcessorFactoryTests.java index 5b03d288064..1b9d88160bd 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/RemoveProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/RemoveProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; @@ -55,7 +56,7 @@ public class RemoveProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/RenameProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/RenameProcessorFactoryTests.java index ea6284f305a..85fc3e71bba 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/RenameProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/RenameProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -50,7 +51,7 @@ public class RenameProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -62,7 +63,7 @@ public class RenameProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[to] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/SetProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/SetProcessorFactoryTests.java index 1c3cf15e48f..2db2dcd5e1c 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/SetProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/SetProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; @@ -58,7 +59,7 @@ public class SetProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -69,7 +70,7 @@ public class SetProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[value] required property is missing")); } } @@ -81,7 +82,7 @@ public class SetProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[value] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/SplitProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/SplitProcessorFactoryTests.java index 3bd2f95e2bc..70fca6f501b 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/SplitProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/SplitProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -50,7 +51,7 @@ public class SplitProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -62,7 +63,7 @@ public class SplitProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[separator] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/TrimProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/TrimProcessorFactoryTests.java index 8012893bfcb..1e74b78f973 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/TrimProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/TrimProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -47,7 +48,7 @@ public class TrimProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/UppercaseProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/UppercaseProcessorFactoryTests.java index 914909f9378..40e14b5f14d 100644 --- a/core/src/test/java/org/elasticsearch/ingest/processor/UppercaseProcessorFactoryTests.java +++ b/core/src/test/java/org/elasticsearch/ingest/processor/UppercaseProcessorFactoryTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.ingest.processor; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.Processor; import org.elasticsearch.test.ESTestCase; @@ -47,7 +48,7 @@ public class UppercaseProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("factory create should have failed"); - } catch(ConfigurationPropertyException e) { + } catch(ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } diff --git a/modules/ingest-grok/src/test/java/org/elasticsearch/ingest/grok/GrokProcessorFactoryTests.java b/modules/ingest-grok/src/test/java/org/elasticsearch/ingest/grok/GrokProcessorFactoryTests.java index 1c36e26925d..db98090af39 100644 --- a/modules/ingest-grok/src/test/java/org/elasticsearch/ingest/grok/GrokProcessorFactoryTests.java +++ b/modules/ingest-grok/src/test/java/org/elasticsearch/ingest/grok/GrokProcessorFactoryTests.java @@ -19,9 +19,8 @@ package org.elasticsearch.ingest.grok; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; -import org.elasticsearch.ingest.core.Processor; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import org.elasticsearch.test.ESTestCase; import java.util.Collections; @@ -54,7 +53,7 @@ public class GrokProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("should fail"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[field] required property is missing")); } @@ -67,7 +66,7 @@ public class GrokProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("should fail"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[pattern] required property is missing")); } diff --git a/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java b/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java index dbcdbbc1a7d..0f51d82de75 100644 --- a/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java +++ b/plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java @@ -29,14 +29,13 @@ import com.maxmind.geoip2.record.Country; import com.maxmind.geoip2.record.Location; import com.maxmind.geoip2.record.Subdivision; import org.apache.lucene.util.IOUtils; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.SpecialPermission; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.ingest.core.AbstractProcessor; import org.elasticsearch.ingest.core.AbstractProcessorFactory; import org.elasticsearch.ingest.core.IngestDocument; -import org.elasticsearch.ingest.core.Processor; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import java.io.Closeable; import java.io.IOException; @@ -52,6 +51,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import static org.elasticsearch.ingest.core.ConfigurationUtils.newConfigurationException; import static org.elasticsearch.ingest.core.ConfigurationUtils.readOptionalList; import static org.elasticsearch.ingest.core.ConfigurationUtils.readStringProperty; @@ -94,7 +94,7 @@ public final class GeoIpProcessor extends AbstractProcessor { } break; default: - throw new IllegalStateException("Unsupported database type [" + dbReader.getMetadata().getDatabaseType() + "]"); + throw new ElasticsearchParseException("Unsupported database type [" + dbReader.getMetadata().getDatabaseType() + "]", new IllegalStateException()); } ingestDocument.setFieldValue(targetField, geoData); } @@ -240,7 +240,7 @@ public final class GeoIpProcessor extends AbstractProcessor { try { fields.add(Field.parse(fieldName)); } catch (Exception e) { - throw new ConfigurationPropertyException(TYPE, processorTag, "fields", "illegal field option [" + fieldName + "]. valid values are [" + Arrays.toString(Field.values()) +"]"); + throw newConfigurationException(TYPE, processorTag, "fields", "illegal field option [" + fieldName + "]. valid values are [" + Arrays.toString(Field.values()) + "]"); } } } else { @@ -249,7 +249,7 @@ public final class GeoIpProcessor extends AbstractProcessor { DatabaseReader databaseReader = databaseReaders.get(databaseFile); if (databaseReader == null) { - throw new ConfigurationPropertyException(TYPE, processorTag, "database_file", "database file [" + databaseFile + "] doesn't exist"); + throw newConfigurationException(TYPE, processorTag, "database_file", "database file [" + databaseFile + "] doesn't exist"); } return new GeoIpProcessor(processorTag, ipField, databaseReader, targetField, fields); } diff --git a/plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorFactoryTests.java b/plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorFactoryTests.java index 410f6e343f7..13143a09651 100644 --- a/plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorFactoryTests.java +++ b/plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorFactoryTests.java @@ -20,9 +20,8 @@ package org.elasticsearch.ingest.geoip; import com.maxmind.geoip2.DatabaseReader; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.core.AbstractProcessorFactory; -import org.elasticsearch.ingest.core.Processor; -import org.elasticsearch.ingest.processor.ConfigurationPropertyException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.StreamsUtils; import org.junit.AfterClass; @@ -113,7 +112,7 @@ public class GeoIpProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("Exception expected"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[database_file] database file [does-not-exist.mmdb] doesn't exist")); } } @@ -146,7 +145,7 @@ public class GeoIpProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("exception expected"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[fields] illegal field option [invalid]. valid values are [[IP, COUNTRY_ISO_CODE, COUNTRY_NAME, CONTINENT_NAME, REGION_NAME, CITY_NAME, TIMEZONE, LATITUDE, LONGITUDE, LOCATION]]")); } @@ -156,7 +155,7 @@ public class GeoIpProcessorFactoryTests extends ESTestCase { try { factory.create(config); fail("exception expected"); - } catch (ConfigurationPropertyException e) { + } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[fields] property isn't a list, but of type [java.lang.String]")); } } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/10_crud.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/10_crud.yaml index 74808331446..c9d7fd451f5 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/10_crud.yaml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/10_crud.yaml @@ -53,6 +53,7 @@ --- "Test invalid processor config": - do: + catch: request ingest.put_pipeline: id: "my_pipeline" body: > @@ -66,12 +67,11 @@ } ] } - - match: { "acknowledged": false } - - length: { "error": 4 } - - match: { "error.reason": "[field] required property is missing" } - - match: { "error.property_name": "field" } - - match: { "error.type": "set" } - - match: { "error.tag": "fritag" } + - match: { error.root_cause.0.type: "parse_exception" } + - match: { error.root_cause.0.reason: "[field] required property is missing" } + - match: { error.root_cause.0.header.processor_tag: "fritag" } + - match: { error.root_cause.0.header.processor_type: "set" } + - match: { error.root_cause.0.header.property_name: "field" } --- "Test basic pipeline with on_failure in processor": diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/40_simulate.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/40_simulate.yaml index 92fad242db9..288806a5f97 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/40_simulate.yaml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/40_simulate.yaml @@ -98,11 +98,11 @@ } ] } - - length: { error: 4 } - - match: { error.tag: "fails" } - - match: { error.type: "set" } - - match: { error.reason: "[field] required property is missing" } - - match: { error.property_name: "field" } + - match: { error.root_cause.0.type: "parse_exception" } + - match: { error.root_cause.0.reason: "[field] required property is missing" } + - match: { error.root_cause.0.header.processor_tag: "fails" } + - match: { error.root_cause.0.header.processor_type: "set" } + - match: { error.root_cause.0.header.property_name: "field" } --- "Test simulate without index type and id": @@ -191,10 +191,9 @@ } ] } - - length: { error: 4 } - - is_false: error.processor_type - - is_false: error.processor_tag - - match: { error.property_name: "pipeline" } + - is_false: error.root_cause.0.header.processor_type + - is_false: error.root_cause.0.header.processor_tag + - match: { error.root_cause.0.header.property_name: "pipeline" } - match: { error.reason: "[pipeline] required property is missing" } --- @@ -225,11 +224,11 @@ } ] } - - length: { error: 4 } - - match: { error.type: "set" } - - is_false: error.tag - - match: { error.reason: "[value] required property is missing" } - - match: { error.property_name: "value" } + - match: { error.root_cause.0.type: "parse_exception" } + - match: { error.root_cause.0.reason: "[value] required property is missing" } + - match: { error.root_cause.0.header.processor_type: "set" } + - match: { error.root_cause.0.header.property_name: "value" } + - is_false: error.root_cause.0.header.processor_tag --- "Test simulate with verbose flag": From 9e7e2ab10b29d22356f3a87df779d9ce41dc0e38 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Tue, 2 Feb 2016 14:16:01 -0800 Subject: [PATCH 45/49] remove DeDotProcessor from Ingest --- .../ingest/processor/DeDotProcessor.java | 106 ------------------ .../org/elasticsearch/node/NodeModule.java | 2 - .../processor/DeDotProcessorFactoryTests.java | 56 --------- .../ingest/processor/DeDotProcessorTests.java | 75 ------------- docs/reference/ingest/ingest.asciidoc | 21 ---- .../test/ingest/80_dedot_processor.yaml | 64 ----------- 6 files changed, 324 deletions(-) delete mode 100644 core/src/main/java/org/elasticsearch/ingest/processor/DeDotProcessor.java delete mode 100644 core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorFactoryTests.java delete mode 100644 core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorTests.java delete mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/ingest/80_dedot_processor.yaml diff --git a/core/src/main/java/org/elasticsearch/ingest/processor/DeDotProcessor.java b/core/src/main/java/org/elasticsearch/ingest/processor/DeDotProcessor.java deleted file mode 100644 index 62063a49fd0..00000000000 --- a/core/src/main/java/org/elasticsearch/ingest/processor/DeDotProcessor.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.processor; - -import org.elasticsearch.ingest.core.AbstractProcessor; -import org.elasticsearch.ingest.core.AbstractProcessorFactory; -import org.elasticsearch.ingest.core.ConfigurationUtils; -import org.elasticsearch.ingest.core.IngestDocument; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Processor that replaces dots in document field names with a - * specified separator. - */ -public class DeDotProcessor extends AbstractProcessor { - - public static final String TYPE = "dedot"; - static final String DEFAULT_SEPARATOR = "_"; - - private final String separator; - - DeDotProcessor(String tag, String separator) { - super(tag); - this.separator = separator; - } - - public String getSeparator() { - return separator; - } - - @Override - public void execute(IngestDocument document) { - deDot(document.getSourceAndMetadata()); - } - - @Override - public String getType() { - return TYPE; - } - - /** - * Recursively iterates through Maps and Lists in search of map entries with - * keys containing dots. The dots in these fields are replaced with {@link #separator}. - * - * @param obj The current object in context to be checked for dots in its fields. - */ - private void deDot(Object obj) { - if (obj instanceof Map) { - @SuppressWarnings("unchecked") - Map doc = (Map) obj; - Iterator> it = doc.entrySet().iterator(); - Map deDottedFields = new HashMap<>(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - deDot(entry.getValue()); - String fieldName = entry.getKey(); - if (fieldName.contains(".")) { - String deDottedFieldName = fieldName.replaceAll("\\.", separator); - deDottedFields.put(deDottedFieldName, entry.getValue()); - it.remove(); - } - } - doc.putAll(deDottedFields); - } else if (obj instanceof List) { - @SuppressWarnings("unchecked") - List list = (List) obj; - for (Object value : list) { - deDot(value); - } - } - } - - public static class Factory extends AbstractProcessorFactory { - - @Override - public DeDotProcessor doCreate(String processorTag, Map config) throws Exception { - String separator = ConfigurationUtils.readOptionalStringProperty(TYPE, processorTag, config, "separator"); - if (separator == null) { - separator = DEFAULT_SEPARATOR; - } - return new DeDotProcessor(processorTag, separator); - } - } -} - diff --git a/core/src/main/java/org/elasticsearch/node/NodeModule.java b/core/src/main/java/org/elasticsearch/node/NodeModule.java index 442dc727007..365e260ebf0 100644 --- a/core/src/main/java/org/elasticsearch/node/NodeModule.java +++ b/core/src/main/java/org/elasticsearch/node/NodeModule.java @@ -28,7 +28,6 @@ import org.elasticsearch.ingest.core.TemplateService; import org.elasticsearch.ingest.processor.AppendProcessor; import org.elasticsearch.ingest.processor.ConvertProcessor; import org.elasticsearch.ingest.processor.DateProcessor; -import org.elasticsearch.ingest.processor.DeDotProcessor; import org.elasticsearch.ingest.processor.FailProcessor; import org.elasticsearch.ingest.processor.GsubProcessor; import org.elasticsearch.ingest.processor.JoinProcessor; @@ -75,7 +74,6 @@ public class NodeModule extends AbstractModule { registerProcessor(ConvertProcessor.TYPE, (templateService) -> new ConvertProcessor.Factory()); registerProcessor(GsubProcessor.TYPE, (templateService) -> new GsubProcessor.Factory()); registerProcessor(FailProcessor.TYPE, FailProcessor.Factory::new); - registerProcessor(DeDotProcessor.TYPE, (templateService) -> new DeDotProcessor.Factory()); } @Override diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorFactoryTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorFactoryTests.java deleted file mode 100644 index 63eee56cc68..00000000000 --- a/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorFactoryTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.processor; - -import org.elasticsearch.ingest.core.AbstractProcessorFactory; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; - -import java.util.HashMap; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.equalTo; - -public class DeDotProcessorFactoryTests extends ESTestCase { - - private DeDotProcessor.Factory factory; - - @Before - public void init() { - factory = new DeDotProcessor.Factory(); - } - - public void testCreate() throws Exception { - Map config = new HashMap<>(); - config.put("separator", "_"); - String processorTag = randomAsciiOfLength(10); - config.put(AbstractProcessorFactory.TAG_KEY, processorTag); - DeDotProcessor deDotProcessor = factory.create(config); - assertThat(deDotProcessor.getSeparator(), equalTo("_")); - assertThat(deDotProcessor.getTag(), equalTo(processorTag)); - } - - public void testCreateMissingSeparatorField() throws Exception { - Map config = new HashMap<>(); - DeDotProcessor deDotProcessor = factory.create(config); - assertThat(deDotProcessor.getSeparator(), equalTo(DeDotProcessor.DEFAULT_SEPARATOR)); - } - -} diff --git a/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorTests.java b/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorTests.java deleted file mode 100644 index a0c87d7a16b..00000000000 --- a/core/src/test/java/org/elasticsearch/ingest/processor/DeDotProcessorTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.ingest.processor; - -import org.elasticsearch.ingest.core.IngestDocument; -import org.elasticsearch.ingest.core.Processor; -import org.elasticsearch.test.ESTestCase; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static org.hamcrest.Matchers.equalTo; - -public class DeDotProcessorTests extends ESTestCase { - - public void testSimple() throws Exception { - Map source = new HashMap<>(); - source.put("a.b", "hello world!"); - IngestDocument ingestDocument = new IngestDocument(source, Collections.emptyMap()); - String separator = randomUnicodeOfCodepointLengthBetween(1, 10); - Processor processor = new DeDotProcessor(randomAsciiOfLength(10), separator); - processor.execute(ingestDocument); - assertThat(ingestDocument.getSourceAndMetadata().get("a" + separator + "b" ), equalTo("hello world!")); - } - - public void testSimpleMap() throws Exception { - Map source = new HashMap<>(); - Map subField = new HashMap<>(); - subField.put("b.c", "hello world!"); - source.put("a", subField); - IngestDocument ingestDocument = new IngestDocument(source, Collections.emptyMap()); - Processor processor = new DeDotProcessor(randomAsciiOfLength(10), "_"); - processor.execute(ingestDocument); - - IngestDocument expectedDocument = new IngestDocument( - Collections.singletonMap("a", Collections.singletonMap("b_c", "hello world!")), - Collections.emptyMap()); - assertThat(ingestDocument, equalTo(expectedDocument)); - } - - public void testSimpleList() throws Exception { - Map source = new HashMap<>(); - Map subField = new HashMap<>(); - subField.put("b.c", "hello world!"); - source.put("a", Arrays.asList(subField)); - IngestDocument ingestDocument = new IngestDocument(source, Collections.emptyMap()); - Processor processor = new DeDotProcessor(randomAsciiOfLength(10), "_"); - processor.execute(ingestDocument); - - IngestDocument expectedDocument = new IngestDocument( - Collections.singletonMap("a", - Collections.singletonList(Collections.singletonMap("b_c", "hello world!"))), - Collections.emptyMap()); - assertThat(ingestDocument, equalTo(expectedDocument)); - } -} diff --git a/docs/reference/ingest/ingest.asciidoc b/docs/reference/ingest/ingest.asciidoc index dfe377ecac4..e9226e7d537 100644 --- a/docs/reference/ingest/ingest.asciidoc +++ b/docs/reference/ingest/ingest.asciidoc @@ -534,27 +534,6 @@ to the requester. } -------------------------------------------------- -==== DeDot Processor -The DeDot Processor is used to remove dots (".") from field names and -replace them with a specific `separator` string. - -[[dedot-options]] -.DeDot Options -[options="header"] -|====== -| Name | Required | Default | Description -| `separator` | yes | "_" | The string to replace dots with in all field names -|====== - -[source,js] --------------------------------------------------- -{ - "dedot": { - "separator": "_" - } -} --------------------------------------------------- - === Accessing data in pipelines diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/80_dedot_processor.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/80_dedot_processor.yaml deleted file mode 100644 index bdc64572a45..00000000000 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/ingest/80_dedot_processor.yaml +++ /dev/null @@ -1,64 +0,0 @@ ---- -"Test De-Dot Processor With Provided Separator": - - do: - ingest.put_pipeline: - id: "my_pipeline" - body: > - { - "description": "_description", - "processors": [ - { - "dedot" : { - "separator" : "3" - } - } - ] - } - - match: { acknowledged: true } - - - do: - index: - index: test - type: test - id: 1 - pipeline: "my_pipeline" - body: {"a.b.c": "hello world"} - - - do: - get: - index: test - type: test - id: 1 - - match: { _source.a3b3c: "hello world" } - ---- -"Test De-Dot Processor With Default Separator": - - do: - ingest.put_pipeline: - id: "my_pipeline" - body: > - { - "description": "_description", - "processors": [ - { - "dedot" : { - } - } - ] - } - - match: { acknowledged: true } - - - do: - index: - index: test - type: test - id: 1 - pipeline: "my_pipeline" - body: {"a.b.c": "hello world"} - - - do: - get: - index: test - type: test - id: 1 - - match: { _source.a_b_c: "hello world" } From a3a49a12ef72782512b2ad36d5c1770747599ddd Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 27 Jan 2016 08:51:18 -0500 Subject: [PATCH 46/49] Illegal shard failure requests Today, shard failure requests are blindly handled on the master without any validation that the request is a legal request. A legal request is a shard failure request for which the shard requesting the failure is either the local allocation or the primary allocation. This is because shard failure requests are classified into only two sets: requests that correspond to shards that exist, and requests that correspond to shards that do not exist. Requests that correspond to shards that do not exist are immediately marked as successful (there is nothing to do), and requests that correspond to shards that do exist are sent to the allocation service for handling the failure. This pull request adds a third classification for shard failure requests to separate out illegal shard failure requests and enables the master to validate shard failure requests. The master communicates the illegality of a shard failure request via a new exception: NoLongerPrimaryShardException. This exception can be used by shard failure listeners to discover when they've sent a shard failure request that they were not allowed to send (e.g., if they are no longer the primary allocation for the shard). Closes #16275 --- .../elasticsearch/ElasticsearchException.java | 4 +- .../TransportReplicationAction.java | 36 +++--- .../cluster/ClusterStateTaskExecutor.java | 5 + .../action/shard/ShardStateAction.java | 104 +++++++++++---- .../cluster/routing/RoutingTable.java | 8 ++ .../cluster/IndicesClusterStateService.java | 40 +++--- .../ExceptionSerializationTests.java | 9 ++ .../TransportReplicationActionTests.java | 14 +- ...rdFailedClusterStateTaskExecutorTests.java | 122 ++++++++++++++---- .../action/shard/ShardStateActionTests.java | 63 ++++++--- .../DiscoveryWithServiceDisruptionsIT.java | 4 +- 11 files changed, 302 insertions(+), 107 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/ElasticsearchException.java b/core/src/main/java/org/elasticsearch/ElasticsearchException.java index e6dc7deff2b..dbbe98633ae 100644 --- a/core/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/core/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -19,6 +19,7 @@ package org.elasticsearch; +import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -613,7 +614,8 @@ public class ElasticsearchException extends RuntimeException implements ToXConte RETRY_ON_REPLICA_EXCEPTION(org.elasticsearch.action.support.replication.TransportReplicationAction.RetryOnReplicaException.class, org.elasticsearch.action.support.replication.TransportReplicationAction.RetryOnReplicaException::new, 136), TYPE_MISSING_EXCEPTION(org.elasticsearch.indices.TypeMissingException.class, org.elasticsearch.indices.TypeMissingException::new, 137), FAILED_TO_COMMIT_CLUSTER_STATE_EXCEPTION(org.elasticsearch.discovery.Discovery.FailedToCommitClusterStateException.class, org.elasticsearch.discovery.Discovery.FailedToCommitClusterStateException::new, 140), - QUERY_SHARD_EXCEPTION(org.elasticsearch.index.query.QueryShardException.class, org.elasticsearch.index.query.QueryShardException::new, 141); + QUERY_SHARD_EXCEPTION(org.elasticsearch.index.query.QueryShardException.class, org.elasticsearch.index.query.QueryShardException::new, 141), + NO_LONGER_PRIMARY_SHARD_EXCEPTION(ShardStateAction.NoLongerPrimaryShardException.class, ShardStateAction.NoLongerPrimaryShardException::new, 142); final Class exceptionClass; final FunctionThatThrowsIOException constructor; diff --git a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index cd7d2871e7f..7f29954762d 100644 --- a/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/core/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -778,16 +778,15 @@ public abstract class TransportReplicationAction shards; private final DiscoveryNodes nodes; private final boolean executeOnReplica; - private final String indexUUID; private final AtomicBoolean finished = new AtomicBoolean(); private final AtomicInteger success = new AtomicInteger(1); // We already wrote into the primary shard private final ConcurrentMap shardReplicaFailures = ConcurrentCollections.newConcurrentMap(); private final AtomicInteger pending; private final int totalShards; - private final Releasable indexShardReference; + private final IndexShardReference indexShardReference; public ReplicationPhase(ReplicaRequest replicaRequest, Response finalResponse, ShardId shardId, - TransportChannel channel, Releasable indexShardReference) { + TransportChannel channel, IndexShardReference indexShardReference) { this.replicaRequest = replicaRequest; this.channel = channel; this.finalResponse = finalResponse; @@ -804,7 +803,6 @@ public abstract class TransportReplicationAction { return this == SUCCESS; } + public Throwable getFailure() { + assert !isSuccess(); + return failure; + } + /** * Handle the execution result with the provided consumers * @param onSuccess handler to invoke on success diff --git a/core/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java b/core/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java index 4aca9a4e235..fa703881bb2 100644 --- a/core/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java +++ b/core/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java @@ -19,6 +19,7 @@ package org.elasticsearch.cluster.action.shard; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.ClusterState; @@ -28,8 +29,9 @@ import org.elasticsearch.cluster.ClusterStateTaskExecutor; import org.elasticsearch.cluster.ClusterStateTaskListener; import org.elasticsearch.cluster.MasterNodeChangePredicate; import org.elasticsearch.cluster.NotMasterException; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingNodes; import org.elasticsearch.cluster.routing.RoutingService; import org.elasticsearch.cluster.routing.ShardRouting; @@ -46,6 +48,7 @@ import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.discovery.Discovery; +import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.node.NodeClosedException; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.ConnectTransportException; @@ -60,6 +63,7 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -125,17 +129,22 @@ public class ShardStateAction extends AbstractComponent { return ExceptionsHelper.unwrap(exp, MASTER_CHANNEL_EXCEPTIONS) != null; } - public void shardFailed(final ShardRouting shardRouting, final String indexUUID, final String message, @Nullable final Throwable failure, Listener listener) { + /** + * Send a shard failed request to the master node to update the + * cluster state. + * + * @param shardRouting the shard to fail + * @param sourceShardRouting the source shard requesting the failure (must be the shard itself, or the primary shard) + * @param message the reason for the failure + * @param failure the underlying cause of the failure + * @param listener callback upon completion of the request + */ + public void shardFailed(final ShardRouting shardRouting, ShardRouting sourceShardRouting, final String message, @Nullable final Throwable failure, Listener listener) { ClusterStateObserver observer = new ClusterStateObserver(clusterService, null, logger, threadPool.getThreadContext()); - ShardRoutingEntry shardRoutingEntry = new ShardRoutingEntry(shardRouting, indexUUID, message, failure); + ShardRoutingEntry shardRoutingEntry = new ShardRoutingEntry(shardRouting, sourceShardRouting, message, failure); sendShardAction(SHARD_FAILED_ACTION_NAME, observer, shardRoutingEntry, listener); } - public void resendShardFailed(final ShardRouting shardRouting, final String indexUUID, final String message, @Nullable final Throwable failure, Listener listener) { - logger.trace("{} re-sending failed shard [{}], index UUID [{}], reason [{}]", shardRouting.shardId(), failure, shardRouting, indexUUID, message); - shardFailed(shardRouting, indexUUID, message, failure, listener); - } - // visible for testing protected void waitForNewMasterAndRetry(String actionName, ClusterStateObserver observer, ShardRoutingEntry shardRoutingEntry, Listener listener) { observer.waitForNextChange(new ClusterStateObserver.Listener() { @@ -231,15 +240,15 @@ public class ShardStateAction extends AbstractComponent { // partition tasks into those that correspond to shards // that exist versus do not exist - Map> partition = - tasks.stream().collect(Collectors.partitioningBy(task -> shardExists(currentState, task))); + Map> partition = + tasks.stream().collect(Collectors.groupingBy(task -> validateTask(currentState, task))); // tasks that correspond to non-existent shards are marked // as successful - batchResultBuilder.successes(partition.get(false)); + batchResultBuilder.successes(partition.getOrDefault(ValidationResult.SHARD_MISSING, Collections.emptyList())); ClusterState maybeUpdatedState = currentState; - List tasksToFail = partition.get(true); + List tasksToFail = partition.getOrDefault(ValidationResult.VALID, Collections.emptyList()); try { List failedShards = tasksToFail @@ -257,6 +266,15 @@ public class ShardStateAction extends AbstractComponent { batchResultBuilder.failures(tasksToFail, t); } + partition + .getOrDefault(ValidationResult.SOURCE_INVALID, Collections.emptyList()) + .forEach(task -> batchResultBuilder.failure( + task, + new NoLongerPrimaryShardException( + task.getShardRouting().shardId(), + "source shard [" + task.sourceShardRouting + "] is neither the local allocation nor the primary allocation") + )); + return batchResultBuilder.build(maybeUpdatedState); } @@ -265,17 +283,36 @@ public class ShardStateAction extends AbstractComponent { return allocationService.applyFailedShards(currentState, failedShards); } - private boolean shardExists(ClusterState currentState, ShardRoutingEntry task) { + private enum ValidationResult { + VALID, + SOURCE_INVALID, + SHARD_MISSING + } + + private ValidationResult validateTask(ClusterState currentState, ShardRoutingEntry task) { + + // non-local requests + if (!task.shardRouting.isSameAllocation(task.sourceShardRouting)) { + IndexShardRoutingTable indexShard = currentState.getRoutingTable().shardRoutingTableOrNull(task.shardRouting.shardId()); + if (indexShard == null) { + return ValidationResult.SOURCE_INVALID; + } + ShardRouting primaryShard = indexShard.primaryShard(); + if (primaryShard == null || !primaryShard.isSameAllocation(task.sourceShardRouting)) { + return ValidationResult.SOURCE_INVALID; + } + } + RoutingNodes.RoutingNodeIterator routingNodeIterator = currentState.getRoutingNodes().routingNodeIter(task.getShardRouting().currentNodeId()); if (routingNodeIterator != null) { for (ShardRouting maybe : routingNodeIterator) { if (task.getShardRouting().isSameAllocation(maybe)) { - return true; + return ValidationResult.VALID; } } } - return false; + return ValidationResult.SHARD_MISSING; } @Override @@ -291,9 +328,9 @@ public class ShardStateAction extends AbstractComponent { } } - public void shardStarted(final ShardRouting shardRouting, String indexUUID, final String message, Listener listener) { + public void shardStarted(final ShardRouting shardRouting, final String message, Listener listener) { ClusterStateObserver observer = new ClusterStateObserver(clusterService, null, logger, threadPool.getThreadContext()); - ShardRoutingEntry shardRoutingEntry = new ShardRoutingEntry(shardRouting, indexUUID, message, null); + ShardRoutingEntry shardRoutingEntry = new ShardRoutingEntry(shardRouting, shardRouting, message, null); sendShardAction(SHARD_STARTED_ACTION_NAME, observer, shardRoutingEntry, listener); } @@ -360,16 +397,16 @@ public class ShardStateAction extends AbstractComponent { public static class ShardRoutingEntry extends TransportRequest { ShardRouting shardRouting; - String indexUUID = IndexMetaData.INDEX_UUID_NA_VALUE; + ShardRouting sourceShardRouting; String message; Throwable failure; public ShardRoutingEntry() { } - ShardRoutingEntry(ShardRouting shardRouting, String indexUUID, String message, @Nullable Throwable failure) { + ShardRoutingEntry(ShardRouting shardRouting, ShardRouting sourceShardRouting, String message, @Nullable Throwable failure) { this.shardRouting = shardRouting; - this.indexUUID = indexUUID; + this.sourceShardRouting = sourceShardRouting; this.message = message; this.failure = failure; } @@ -382,7 +419,7 @@ public class ShardStateAction extends AbstractComponent { public void readFrom(StreamInput in) throws IOException { super.readFrom(in); shardRouting = readShardRoutingEntry(in); - indexUUID = in.readString(); + sourceShardRouting = readShardRoutingEntry(in); message = in.readString(); failure = in.readThrowable(); } @@ -391,18 +428,25 @@ public class ShardStateAction extends AbstractComponent { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardRouting.writeTo(out); - out.writeString(indexUUID); + sourceShardRouting.writeTo(out); out.writeString(message); out.writeThrowable(failure); } @Override public String toString() { - return "" + shardRouting + ", indexUUID [" + indexUUID + "], message [" + message + "], failure [" + ExceptionsHelper.detailedMessage(failure) + "]"; + return String.format( + Locale.ROOT, + "failed shard [%s], source shard [%s], message [%s], failure [%s]", + shardRouting, + sourceShardRouting, + message, + ExceptionsHelper.detailedMessage(failure)); } } public interface Listener { + default void onSuccess() { } @@ -423,6 +467,20 @@ public class ShardStateAction extends AbstractComponent { */ default void onFailure(final Throwable t) { } + + } + + public static class NoLongerPrimaryShardException extends ElasticsearchException { + + public NoLongerPrimaryShardException(ShardId shardId, String msg) { + super(msg); + setShard(shardId); + } + + public NoLongerPrimaryShardException(StreamInput in) throws IOException { + super(in); + } + } } diff --git a/core/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java b/core/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java index 6d81556eb2c..58e3ed6b644 100644 --- a/core/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java +++ b/core/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Predicate; /** @@ -137,6 +138,13 @@ public class RoutingTable implements Iterable, Diffable Optional.ofNullable(irt.shard(shardId.getId()))) + .orElse(null); + } + public RoutingTable validateRaiseException(MetaData metaData) throws RoutingValidationException { RoutingTableValidation validation = validate(metaData); if (!validation.valid()) { diff --git a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index f5afec1d5e3..bd94fb8a123 100644 --- a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -306,7 +306,7 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent { try { if (indexShard.recoverFromStore(nodes.localNode())) { - shardStateAction.shardStarted(shardRouting, indexMetaData.getIndexUUID(), "after recovery from store", SHARD_STATE_ACTION_LISTENER); + shardStateAction.shardStarted(shardRouting, "after recovery from store", SHARD_STATE_ACTION_LISTENER); } } catch (Throwable t) { handleRecoveryFailure(indexService, shardRouting, true, t); @@ -634,7 +636,7 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent { synchronized (mutex) { - failAndRemoveShard(shardRouting, shardFailure.indexUUID, indexService, true, "shard failure, reason [" + shardFailure.reason + "]", shardFailure.cause); + failAndRemoveShard(shardRouting, indexService, true, "shard failure, reason [" + shardFailure.reason + "]", shardFailure.cause); } }); } diff --git a/core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java b/core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java index 50764eef65e..0b693aed56b 100644 --- a/core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java +++ b/core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.TimestampParsingException; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.client.AbstractClientHeadersTestCase; +import org.elasticsearch.cluster.action.shard.ShardStateAction; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.metadata.SnapshotId; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -591,7 +592,14 @@ public class ExceptionSerializationTests extends ESTestCase { assertEquals("foo", e.getHeader("foo").get(0)); assertEquals("bar", e.getHeader("foo").get(1)); assertSame(status, e.status()); + } + public void testNoLongerPrimaryShardException() throws IOException { + ShardId shardId = new ShardId(new Index(randomAsciiOfLength(4), randomAsciiOfLength(4)), randomIntBetween(0, Integer.MAX_VALUE)); + String msg = randomAsciiOfLength(4); + ShardStateAction.NoLongerPrimaryShardException ex = serialize(new ShardStateAction.NoLongerPrimaryShardException(shardId, msg)); + assertEquals(shardId, ex.getShardId()); + assertEquals(msg, ex.getMessage()); } public static class UnknownHeaderException extends ElasticsearchException { @@ -776,6 +784,7 @@ public class ExceptionSerializationTests extends ESTestCase { ids.put(139, null); ids.put(140, org.elasticsearch.discovery.Discovery.FailedToCommitClusterStateException.class); ids.put(141, org.elasticsearch.index.query.QueryShardException.class); + ids.put(142, ShardStateAction.NoLongerPrimaryShardException.class); Map, Integer> reverse = new HashMap<>(); for (Map.Entry> entry : ids.entrySet()) { diff --git a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java index ce9343309a6..2e4e3cb475b 100644 --- a/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java +++ b/core/src/test/java/org/elasticsearch/action/support/replication/TransportReplicationActionTests.java @@ -550,7 +550,7 @@ public class TransportReplicationActionTests extends ESTestCase { } } - runReplicateTest(shardRoutingTable, assignedReplicas, totalShards); + runReplicateTest(state, shardRoutingTable, assignedReplicas, totalShards); } public void testReplicationWithShadowIndex() throws ExecutionException, InterruptedException { @@ -581,18 +581,22 @@ public class TransportReplicationActionTests extends ESTestCase { totalShards++; } } - runReplicateTest(shardRoutingTable, assignedReplicas, totalShards); + runReplicateTest(state, shardRoutingTable, assignedReplicas, totalShards); } - protected void runReplicateTest(IndexShardRoutingTable shardRoutingTable, int assignedReplicas, int totalShards) throws InterruptedException, ExecutionException { + protected void runReplicateTest(ClusterState state, IndexShardRoutingTable shardRoutingTable, int assignedReplicas, int totalShards) throws InterruptedException, ExecutionException { final ShardIterator shardIt = shardRoutingTable.shardsIt(); final ShardId shardId = shardIt.shardId(); final Request request = new Request(shardId); final PlainActionFuture listener = new PlainActionFuture<>(); logger.debug("expecting [{}] assigned replicas, [{}] total shards. using state: \n{}", assignedReplicas, totalShards, clusterService.state().prettyPrint()); - Releasable reference = getOrCreateIndexShardOperationsCounter(); + TransportReplicationAction.IndexShardReference reference = getOrCreateIndexShardOperationsCounter(); + + ShardRouting primaryShard = state.getRoutingTable().shardRoutingTable(shardId).primaryShard(); + indexShardRouting.set(primaryShard); + assertIndexShardCounter(2); // TODO: set a default timeout TransportReplicationAction.ReplicationPhase replicationPhase = @@ -755,6 +759,8 @@ public class TransportReplicationActionTests extends ESTestCase { // one replica to make sure replication is attempted clusterService.setState(state(index, true, ShardRoutingState.STARTED, ShardRoutingState.STARTED)); + ShardRouting primaryShard = clusterService.state().routingTable().shardRoutingTable(shardId).primaryShard(); + indexShardRouting.set(primaryShard); logger.debug("--> using initial state:\n{}", clusterService.state().prettyPrint()); Request request = new Request(shardId).timeout("100ms"); PlainActionFuture listener = new PlainActionFuture<>(); diff --git a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardFailedClusterStateTaskExecutorTests.java b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardFailedClusterStateTaskExecutorTests.java index 4e8d1d9266d..3ad8d5013b2 100644 --- a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardFailedClusterStateTaskExecutorTests.java +++ b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardFailedClusterStateTaskExecutorTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.cluster.action.shard; +import com.carrotsearch.hppc.cursors.ObjectCursor; import org.apache.lucene.index.CorruptIndexException; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; @@ -28,6 +29,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.GroupShardsIterator; +import org.elasticsearch.cluster.routing.IndexShardRoutingTable; import org.elasticsearch.cluster.routing.RoutingNodes; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardIterator; @@ -38,6 +40,9 @@ import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.routing.allocation.FailedRerouteAllocation; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; import org.elasticsearch.cluster.routing.allocation.decider.ClusterRebalanceAllocationDecider; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.discovery.DiscoveryService; +import org.elasticsearch.index.Index; import org.elasticsearch.test.ESAllocationTestCase; import org.junit.Before; @@ -45,12 +50,15 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.not; public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCase { @@ -119,9 +127,25 @@ public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCa tasks.addAll(failingTasks); tasks.addAll(nonExistentTasks); ClusterStateTaskExecutor.BatchResult result = failingExecutor.execute(currentState, tasks); - Map taskResultMap = - failingTasks.stream().collect(Collectors.toMap(Function.identity(), task -> false)); - taskResultMap.putAll(nonExistentTasks.stream().collect(Collectors.toMap(Function.identity(), task -> true))); + Map taskResultMap = + failingTasks.stream().collect(Collectors.toMap(Function.identity(), task -> ClusterStateTaskExecutor.TaskResult.failure(new RuntimeException("simulated applyFailedShards failure")))); + taskResultMap.putAll(nonExistentTasks.stream().collect(Collectors.toMap(Function.identity(), task -> ClusterStateTaskExecutor.TaskResult.success()))); + assertTaskResults(taskResultMap, result, currentState, false); + } + + public void testIllegalShardFailureRequests() throws Exception { + String reason = "test illegal shard failure requests"; + ClusterState currentState = createClusterStateWithStartedShards(reason); + List failingTasks = createExistingShards(currentState, reason); + List tasks = new ArrayList<>(); + for (ShardStateAction.ShardRoutingEntry failingTask : failingTasks) { + tasks.add(new ShardStateAction.ShardRoutingEntry(failingTask.getShardRouting(), randomInvalidSourceShard(currentState, failingTask.getShardRouting()), failingTask.message, failingTask.failure)); + } + Map taskResultMap = + tasks.stream().collect(Collectors.toMap( + Function.identity(), + task -> ClusterStateTaskExecutor.TaskResult.failure(new ShardStateAction.NoLongerPrimaryShardException(task.getShardRouting().shardId(), "source shard [" + task.sourceShardRouting + "] is neither the local allocation nor the primary allocation")))); + ClusterStateTaskExecutor.BatchResult result = executor.execute(currentState, tasks); assertTaskResults(taskResultMap, result, currentState, false); } @@ -156,17 +180,22 @@ public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCa for (int i = 0; i < numberOfTasks; i++) { shardsToFail.add(randomFrom(failures)); } - return toTasks(shardsToFail, indexUUID, reason); + return toTasks(currentState, shardsToFail, indexUUID, reason); } private List createNonExistentShards(ClusterState currentState, String reason) { // add shards from a non-existent index - MetaData nonExistentMetaData = - MetaData.builder() - .put(IndexMetaData.builder("non-existent").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(numberOfReplicas)) - .build(); - RoutingTable routingTable = RoutingTable.builder().addAsNew(nonExistentMetaData.index("non-existent")).build(); - String nonExistentIndexUUID = nonExistentMetaData.index("non-existent").getIndexUUID(); + String nonExistentIndexUUID = "non-existent"; + Index index = new Index("non-existent", nonExistentIndexUUID); + List nodeIds = new ArrayList<>(); + for (ObjectCursor nodeId : currentState.nodes().getNodes().keys()) { + nodeIds.add(nodeId.toString()); + } + List nonExistentShards = new ArrayList<>(); + nonExistentShards.add(nonExistentShardRouting(index, nodeIds, true)); + for (int i = 0; i < numberOfReplicas; i++) { + nonExistentShards.add(nonExistentShardRouting(index, nodeIds, false)); + } List existingShards = createExistingShards(currentState, reason); List shardsWithMismatchedAllocationIds = new ArrayList<>(); @@ -174,28 +203,32 @@ public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCa ShardRouting sr = existingShard.getShardRouting(); ShardRouting nonExistentShardRouting = TestShardRouting.newShardRouting(sr.index(), sr.id(), sr.currentNodeId(), sr.relocatingNodeId(), sr.restoreSource(), sr.primary(), sr.state(), sr.version()); - shardsWithMismatchedAllocationIds.add(new ShardStateAction.ShardRoutingEntry(nonExistentShardRouting, existingShard.indexUUID, existingShard.message, existingShard.failure)); + shardsWithMismatchedAllocationIds.add(new ShardStateAction.ShardRoutingEntry(nonExistentShardRouting, nonExistentShardRouting, existingShard.message, existingShard.failure)); } List tasks = new ArrayList<>(); - tasks.addAll(toTasks(routingTable.allShards(), nonExistentIndexUUID, reason)); + nonExistentShards.forEach(shard -> tasks.add(new ShardStateAction.ShardRoutingEntry(shard, shard, reason, new CorruptIndexException("simulated", nonExistentIndexUUID)))); tasks.addAll(shardsWithMismatchedAllocationIds); return tasks; } + private ShardRouting nonExistentShardRouting(Index index, List nodeIds, boolean primary) { + return TestShardRouting.newShardRouting(index, 0, randomFrom(nodeIds), primary, randomFrom(ShardRoutingState.INITIALIZING, ShardRoutingState.RELOCATING, ShardRoutingState.STARTED), randomIntBetween(1, 8)); + } + private static void assertTasksSuccessful( List tasks, ClusterStateTaskExecutor.BatchResult result, ClusterState clusterState, boolean clusterStateChanged ) { - Map taskResultMap = - tasks.stream().collect(Collectors.toMap(Function.identity(), task -> true)); + Map taskResultMap = + tasks.stream().collect(Collectors.toMap(Function.identity(), task -> ClusterStateTaskExecutor.TaskResult.success())); assertTaskResults(taskResultMap, result, clusterState, clusterStateChanged); } private static void assertTaskResults( - Map taskResultMap, + Map taskResultMap, ClusterStateTaskExecutor.BatchResult result, ClusterState clusterState, boolean clusterStateChanged @@ -203,24 +236,29 @@ public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCa // there should be as many task results as tasks assertEquals(taskResultMap.size(), result.executionResults.size()); - for (Map.Entry entry : taskResultMap.entrySet()) { + for (Map.Entry entry : taskResultMap.entrySet()) { // every task should have a corresponding task result assertTrue(result.executionResults.containsKey(entry.getKey())); // the task results are as expected - assertEquals(entry.getValue(), result.executionResults.get(entry.getKey()).isSuccess()); + assertEquals(entry.getValue().isSuccess(), result.executionResults.get(entry.getKey()).isSuccess()); } - // every shard that we requested to be successfully failed is - // gone List shards = clusterState.getRoutingTable().allShards(); - for (Map.Entry entry : taskResultMap.entrySet()) { - if (entry.getValue()) { + for (Map.Entry entry : taskResultMap.entrySet()) { + if (entry.getValue().isSuccess()) { + // the shard was successfully failed and so should not + // be in the routing table for (ShardRouting shard : shards) { if (entry.getKey().getShardRouting().allocationId() != null) { assertThat(shard.allocationId(), not(equalTo(entry.getKey().getShardRouting().allocationId()))); } } + } else { + // check we saw the expected failure + ClusterStateTaskExecutor.TaskResult actualResult = result.executionResults.get(entry.getKey()); + assertThat(actualResult.getFailure(), instanceOf(entry.getValue().getFailure().getClass())); + assertThat(actualResult.getFailure().getMessage(), equalTo(entry.getValue().getFailure().getMessage())); } } @@ -231,11 +269,49 @@ public class ShardFailedClusterStateTaskExecutorTests extends ESAllocationTestCa } } - private static List toTasks(List shards, String indexUUID, String message) { + private static List toTasks(ClusterState currentState, List shards, String indexUUID, String message) { return shards .stream() - .map(shard -> new ShardStateAction.ShardRoutingEntry(shard, indexUUID, message, new CorruptIndexException("simulated", indexUUID))) + .map(shard -> new ShardStateAction.ShardRoutingEntry(shard, randomValidSourceShard(currentState, shard), message, new CorruptIndexException("simulated", indexUUID))) .collect(Collectors.toList()); } + private static ShardRouting randomValidSourceShard(ClusterState currentState, ShardRouting shardRouting) { + // for the request node ID to be valid, either the request is + // from the node the shard is assigned to, or the request is + // from the node holding the primary shard + if (randomBoolean()) { + // request from local node + return shardRouting; + } else { + // request from primary node unless in the case of + // non-existent shards there is not one and we fallback to + // the local node + ShardRouting primaryNodeId = primaryShard(currentState, shardRouting); + return primaryNodeId != null ? primaryNodeId : shardRouting; + } + } + + private static ShardRouting randomInvalidSourceShard(ClusterState currentState, ShardRouting shardRouting) { + ShardRouting primaryShard = primaryShard(currentState, shardRouting); + Set shards = + currentState + .routingTable() + .allShards() + .stream() + .filter(shard -> !shard.isSameAllocation(shardRouting)) + .filter(shard -> !shard.isSameAllocation(primaryShard)) + .collect(Collectors.toSet()); + if (!shards.isEmpty()) { + return randomSubsetOf(1, shards.toArray(new ShardRouting[0])).get(0); + } else { + return + TestShardRouting.newShardRouting(shardRouting.index(), shardRouting.id(), DiscoveryService.generateNodeId(Settings.EMPTY), randomBoolean(), randomFrom(ShardRoutingState.values()), shardRouting.version()); + } + } + + private static ShardRouting primaryShard(ClusterState currentState, ShardRouting shardRouting) { + IndexShardRoutingTable indexShard = currentState.getRoutingTable().shardRoutingTableOrNull(shardRouting.shardId()); + return indexShard == null ? null : indexShard.primaryShard(); + } } diff --git a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java index b6e69b27a5a..62f32e20fec 100644 --- a/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java +++ b/core/src/test/java/org/elasticsearch/cluster/action/shard/ShardStateActionTests.java @@ -30,7 +30,9 @@ import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.routing.RoutingService; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.ShardsIterator; +import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.Discovery; @@ -128,13 +130,11 @@ public class ShardStateActionTests extends ESTestCase { clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); - String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); - AtomicBoolean success = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(1); ShardRouting shardRouting = getRandomShardRouting(index); - shardStateAction.shardFailed(shardRouting, indexUUID, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + shardStateAction.shardFailed(shardRouting, shardRouting, "test", getSimulatedFailure(), new ShardStateAction.Listener() { @Override public void onSuccess() { success.set(true); @@ -174,15 +174,14 @@ public class ShardStateActionTests extends ESTestCase { noMasterBuilder.masterNodeId(null); clusterService.setState(ClusterState.builder(clusterService.state()).nodes(noMasterBuilder)); - String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); - CountDownLatch latch = new CountDownLatch(1); AtomicInteger retries = new AtomicInteger(); AtomicBoolean success = new AtomicBoolean(); setUpMasterRetryVerification(1, retries, latch, requestId -> {}); - shardStateAction.shardFailed(getRandomShardRouting(index), indexUUID, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + ShardRouting failedShard = getRandomShardRouting(index); + shardStateAction.shardFailed(failedShard, failedShard, "test", getSimulatedFailure(), new ShardStateAction.Listener() { @Override public void onSuccess() { success.set(true); @@ -208,8 +207,6 @@ public class ShardStateActionTests extends ESTestCase { clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); - String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); - CountDownLatch latch = new CountDownLatch(1); AtomicInteger retries = new AtomicInteger(); AtomicBoolean success = new AtomicBoolean(); @@ -232,7 +229,8 @@ public class ShardStateActionTests extends ESTestCase { final int numberOfRetries = randomIntBetween(1, 256); setUpMasterRetryVerification(numberOfRetries, retries, latch, retryLoop); - shardStateAction.shardFailed(getRandomShardRouting(index), indexUUID, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + ShardRouting failedShard = getRandomShardRouting(index); + shardStateAction.shardFailed(failedShard, failedShard, "test", getSimulatedFailure(), new ShardStateAction.Listener() { @Override public void onSuccess() { success.set(true); @@ -265,11 +263,10 @@ public class ShardStateActionTests extends ESTestCase { clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); - String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); - AtomicBoolean failure = new AtomicBoolean(); - shardStateAction.shardFailed(getRandomShardRouting(index), indexUUID, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + ShardRouting failedShard = getRandomShardRouting(index); + shardStateAction.shardFailed(failedShard, failedShard, "test", getSimulatedFailure(), new ShardStateAction.Listener() { @Override public void onSuccess() { failure.set(false); @@ -295,15 +292,13 @@ public class ShardStateActionTests extends ESTestCase { clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); - String indexUUID = clusterService.state().metaData().index(index).getIndexUUID(); - AtomicBoolean success = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(1); ShardRouting failedShard = getRandomShardRouting(index); RoutingTable routingTable = RoutingTable.builder(clusterService.state().getRoutingTable()).remove(index).build(); clusterService.setState(ClusterState.builder(clusterService.state()).routingTable(routingTable)); - shardStateAction.shardFailed(failedShard, indexUUID, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + shardStateAction.shardFailed(failedShard, failedShard, "test", getSimulatedFailure(), new ShardStateAction.Listener() { @Override public void onSuccess() { success.set(true); @@ -325,6 +320,44 @@ public class ShardStateActionTests extends ESTestCase { assertTrue(success.get()); } + public void testNoLongerPrimaryShardException() throws InterruptedException { + final String index = "test"; + + clusterService.setState(ClusterStateCreationUtils.stateWithActivePrimary(index, true, randomInt(5))); + + ShardRouting failedShard = getRandomShardRouting(index); + + String nodeId = randomFrom(clusterService.state().nodes().nodes().keys().toArray(String.class)); + + AtomicReference failure = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + ShardRouting sourceFailedShard = TestShardRouting.newShardRouting(failedShard.index(), failedShard.id(), nodeId, randomBoolean(), randomFrom(ShardRoutingState.values()), failedShard.version()); + shardStateAction.shardFailed(failedShard, sourceFailedShard, "test", getSimulatedFailure(), new ShardStateAction.Listener() { + @Override + public void onSuccess() { + failure.set(null); + latch.countDown(); + } + + @Override + public void onFailure(Throwable t) { + failure.set(t); + latch.countDown(); + } + }); + + ShardStateAction.NoLongerPrimaryShardException catastrophicError = + new ShardStateAction.NoLongerPrimaryShardException(failedShard.shardId(), "source shard [" + sourceFailedShard + " is neither the local allocation nor the primary allocation"); + CapturingTransport.CapturedRequest[] capturedRequests = transport.getCapturedRequestsAndClear(); + transport.handleRemoteError(capturedRequests[0].requestId, catastrophicError); + + latch.await(); + assertNotNull(failure.get()); + assertThat(failure.get(), instanceOf(ShardStateAction.NoLongerPrimaryShardException.class)); + assertThat(failure.get().getMessage(), equalTo(catastrophicError.getMessage())); + } + private ShardRouting getRandomShardRouting(String index) { IndexRoutingTable indexRoutingTable = clusterService.state().routingTable().index(index); ShardsIterator shardsIterator = indexRoutingTable.randomAllActiveShardsIt(); diff --git a/core/src/test/java/org/elasticsearch/discovery/DiscoveryWithServiceDisruptionsIT.java b/core/src/test/java/org/elasticsearch/discovery/DiscoveryWithServiceDisruptionsIT.java index c282f3ef183..739e07df4a9 100644 --- a/core/src/test/java/org/elasticsearch/discovery/DiscoveryWithServiceDisruptionsIT.java +++ b/core/src/test/java/org/elasticsearch/discovery/DiscoveryWithServiceDisruptionsIT.java @@ -56,7 +56,6 @@ import org.elasticsearch.discovery.zen.ping.ZenPing; import org.elasticsearch.discovery.zen.ping.ZenPingService; import org.elasticsearch.discovery.zen.ping.unicast.UnicastZenPing; import org.elasticsearch.discovery.zen.publish.PublishClusterStateAction; -import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.indices.store.IndicesStoreIntegrationIT; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -905,7 +904,6 @@ public class DiscoveryWithServiceDisruptionsIT extends ESIntegTestCase { ShardRouting failedShard = randomFrom(clusterService().state().getRoutingNodes().node(nonMasterNodeId).shardsWithState(ShardRoutingState.STARTED)); ShardStateAction service = internalCluster().getInstance(ShardStateAction.class, nonMasterNode); - String indexUUID = clusterService().state().metaData().index("test").getIndexUUID(); CountDownLatch latch = new CountDownLatch(1); AtomicBoolean success = new AtomicBoolean(); @@ -913,7 +911,7 @@ public class DiscoveryWithServiceDisruptionsIT extends ESIntegTestCase { NetworkPartition networkPartition = addRandomIsolation(isolatedNode); networkPartition.startDisrupting(); - service.shardFailed(failedShard, indexUUID, "simulated", new CorruptIndexException("simulated", (String) null), new ShardStateAction.Listener() { + service.shardFailed(failedShard, failedShard, "simulated", new CorruptIndexException("simulated", (String) null), new ShardStateAction.Listener() { @Override public void onSuccess() { success.set(true); From 8b37827ac636b53e55f04c505d06e202e4535768 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 2 Feb 2016 18:14:37 -0500 Subject: [PATCH 47/49] Guard against null routing node iterator --- .../indices/cluster/IndicesClusterStateService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index bd94fb8a123..98bbd5fe000 100644 --- a/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/core/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -415,6 +415,12 @@ public class IndicesClusterStateService extends AbstractLifecycleComponent Date: Tue, 2 Feb 2016 14:39:44 -0800 Subject: [PATCH 48/49] fix ingest client put error test --- .../test/java/org/elasticsearch/ingest/IngestClientIT.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java b/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java index a2995964a0d..e5fcba2a3be 100644 --- a/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java +++ b/core/src/test/java/org/elasticsearch/ingest/IngestClientIT.java @@ -20,6 +20,7 @@ package org.elasticsearch.ingest; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; @@ -219,7 +220,9 @@ public class IngestClientIT extends ESIntegTestCase { try { client().admin().cluster().putPipeline(putPipelineRequest).get(); } catch (ExecutionException e) { - assertThat(e.getCause().getCause().getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); + ElasticsearchParseException ex = (ElasticsearchParseException) ExceptionsHelper.unwrap(e, ElasticsearchParseException.class); + assertNotNull(ex); + assertThat(ex.getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); } } From 900133f1b303df1148c84a9c49debd3b28e60785 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 2 Feb 2016 18:49:31 -0500 Subject: [PATCH 49/49] Suppress checkstyle on generated files in painless Closes #16387 --- .../gradle/precommit/PrecommitTasks.groovy | 3 +++ buildSrc/src/main/resources/checkstyle.xml | 4 ++++ .../src/main/resources/checkstyle_suppressions.xml | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 buildSrc/src/main/resources/checkstyle_suppressions.xml diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index d4d4d08c393..1cdeec762d2 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -94,6 +94,9 @@ class PrecommitTasks { project.checkstyle { config = project.resources.text.fromFile( PrecommitTasks.getResource('/checkstyle.xml'), 'UTF-8') + configProperties = [ + suppressions: PrecommitTasks.getResource('/checkstyle_suppressions.xml') + ] } for (String taskName : ['checkstyleMain', 'checkstyleTest']) { Task task = project.tasks.findByName(taskName) diff --git a/buildSrc/src/main/resources/checkstyle.xml b/buildSrc/src/main/resources/checkstyle.xml index b44c649a52b..4dd0534fa01 100644 --- a/buildSrc/src/main/resources/checkstyle.xml +++ b/buildSrc/src/main/resources/checkstyle.xml @@ -6,6 +6,10 @@ + + + + + + +