diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java index 6fa3a66a79ca6..9394495313df4 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java @@ -909,6 +909,33 @@ public void splitAsync(ResizeRequest resizeRequest, RequestOptions options, Acti ResizeResponse::fromXContent, listener, emptySet()); } + /** + * Clones an index using the Clone Index API. + * See + * Clone Index API on elastic.co + * @param resizeRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public ResizeResponse clone(ResizeRequest resizeRequest, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(resizeRequest, IndicesRequestConverters::clone, options, + ResizeResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously clones an index using the Clone Index API. + * See + * Clone Index API on elastic.co + * @param resizeRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void cloneAsync(ResizeRequest resizeRequest, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(resizeRequest, IndicesRequestConverters::clone, options, + ResizeResponse::fromXContent, listener, emptySet()); + } + /** * Rolls over an index using the Rollover Index API. * See diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java index c0b2f565e534f..62f041d6c6801 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesRequestConverters.java @@ -337,6 +337,13 @@ static Request shrink(ResizeRequest resizeRequest) throws IOException { return resize(resizeRequest); } + static Request clone(ResizeRequest resizeRequest) throws IOException { + if (resizeRequest.getResizeType() != ResizeType.CLONE) { + throw new IllegalArgumentException("Wrong resize type [" + resizeRequest.getResizeType() + "] for indices clone request"); + } + return resize(resizeRequest); + } + private static Request resize(ResizeRequest resizeRequest) throws IOException { String endpoint = new RequestConverters.EndpointBuilder().addPathPart(resizeRequest.getSourceIndex()) .addPathPartAsIs("_" + resizeRequest.getResizeType().name().toLowerCase(Locale.ROOT)) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java index a3c40d6a9d7c3..17b3121cd0b62 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java @@ -1128,6 +1128,30 @@ public void testSplit() throws IOException { assertNotNull(aliasData); } + @SuppressWarnings("unchecked") + public void testClone() throws IOException { + createIndex("source", Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 0) + .put("index.number_of_routing_shards", 4).build()); + updateIndexSettings("source", Settings.builder().put("index.blocks.write", true)); + + ResizeRequest resizeRequest = new ResizeRequest("target", "source"); + resizeRequest.setResizeType(ResizeType.CLONE); + Settings targetSettings = Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 0).build(); + resizeRequest.setTargetIndex(new org.elasticsearch.action.admin.indices.create.CreateIndexRequest("target") + .settings(targetSettings) + .alias(new Alias("alias"))); + ResizeResponse resizeResponse = execute(resizeRequest, highLevelClient().indices()::clone, highLevelClient().indices()::cloneAsync); + assertTrue(resizeResponse.isAcknowledged()); + assertTrue(resizeResponse.isShardsAcknowledged()); + Map getIndexResponse = getAsMap("target"); + Map indexSettings = (Map)XContentMapValues.extractValue("target.settings.index", getIndexResponse); + assertNotNull(indexSettings); + assertEquals("2", indexSettings.get("number_of_shards")); + assertEquals("0", indexSettings.get("number_of_replicas")); + Map aliasData = (Map)XContentMapValues.extractValue("target.aliases.alias", getIndexResponse); + assertNotNull(aliasData); + } + public void testRollover() throws IOException { highLevelClient().indices().create(new CreateIndexRequest("test").alias(new Alias("alias")), RequestOptions.DEFAULT); RolloverRequest rolloverRequest = new RolloverRequest("alias", "test_new"); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesRequestConvertersTests.java index a8728b90023f9..ee3ec3b50cc46 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesRequestConvertersTests.java @@ -830,18 +830,33 @@ public void testSplit() throws IOException { public void testSplitWrongResizeType() { ResizeRequest resizeRequest = new ResizeRequest("target", "source"); - resizeRequest.setResizeType(ResizeType.SHRINK); + ResizeType wrongType = randomFrom(ResizeType.SHRINK, ResizeType.CLONE); + resizeRequest.setResizeType(wrongType); IllegalArgumentException iae = LuceneTestCase.expectThrows(IllegalArgumentException.class, () -> IndicesRequestConverters.split(resizeRequest)); - Assert.assertEquals("Wrong resize type [SHRINK] for indices split request", iae.getMessage()); + Assert.assertEquals("Wrong resize type [" + wrongType.name() + "] for indices split request", iae.getMessage()); + } + + public void testClone() throws IOException { + resizeTest(ResizeType.CLONE, IndicesRequestConverters::clone); + } + + public void testCloneWrongResizeType() { + ResizeRequest resizeRequest = new ResizeRequest("target", "source"); + ResizeType wrongType = randomFrom(ResizeType.SHRINK, ResizeType.SPLIT); + resizeRequest.setResizeType(wrongType); + IllegalArgumentException iae = LuceneTestCase.expectThrows(IllegalArgumentException.class, () + -> IndicesRequestConverters.clone(resizeRequest)); + Assert.assertEquals("Wrong resize type [" + wrongType.name() + "] for indices clone request", iae.getMessage()); } public void testShrinkWrongResizeType() { ResizeRequest resizeRequest = new ResizeRequest("target", "source"); - resizeRequest.setResizeType(ResizeType.SPLIT); + ResizeType wrongType = randomFrom(ResizeType.SPLIT, ResizeType.CLONE); + resizeRequest.setResizeType(wrongType); IllegalArgumentException iae = LuceneTestCase.expectThrows(IllegalArgumentException.class, () -> IndicesRequestConverters.shrink(resizeRequest)); - Assert.assertEquals("Wrong resize type [SPLIT] for indices shrink request", iae.getMessage()); + Assert.assertEquals("Wrong resize type [" + wrongType.name() + "] for indices shrink request", iae.getMessage()); } public void testShrink() throws IOException { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java index ddd9241a49316..6a01400e006a1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java @@ -1808,6 +1808,75 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + public void testCloneIndex() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + createIndex("source_index", Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 0).build()); + updateIndexSettings("source_index", Settings.builder().put("index.blocks.write", true)); + } + + // tag::clone-index-request + ResizeRequest request = new ResizeRequest("target_index","source_index"); // <1> + request.setResizeType(ResizeType.CLONE); // <2> + // end::clone-index-request + + // tag::clone-index-request-timeout + request.timeout(TimeValue.timeValueMinutes(2)); // <1> + request.timeout("2m"); // <2> + // end::clone-index-request-timeout + // tag::clone-index-request-masterTimeout + request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1> + request.masterNodeTimeout("1m"); // <2> + // end::clone-index-request-masterTimeout + // tag::clone-index-request-waitForActiveShards + request.setWaitForActiveShards(2); // <1> + request.setWaitForActiveShards(ActiveShardCount.DEFAULT); // <2> + // end::clone-index-request-waitForActiveShards + // tag::clone-index-request-settings + request.getTargetIndexRequest().settings(Settings.builder() + .put("index.number_of_shards", 2)); // <1> + // end::clone-index-request-settings + // tag::clone-index-request-aliases + request.getTargetIndexRequest().alias(new Alias("target_alias")); // <1> + // end::clone-index-request-aliases + + // tag::clone-index-execute + ResizeResponse resizeResponse = client.indices().clone(request, RequestOptions.DEFAULT); + // end::clone-index-execute + + // tag::clone-index-response + boolean acknowledged = resizeResponse.isAcknowledged(); // <1> + boolean shardsAcked = resizeResponse.isShardsAcknowledged(); // <2> + // end::clone-index-response + assertTrue(acknowledged); + assertTrue(shardsAcked); + + // tag::clone-index-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(ResizeResponse resizeResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::clone-index-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::clone-index-execute-async + client.indices().cloneAsync(request, RequestOptions.DEFAULT,listener); // <1> + // end::clone-index-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + public void testRolloverIndex() throws Exception { RestHighLevelClient client = highLevelClient(); diff --git a/docs/java-rest/high-level/indices/clone_index.asciidoc b/docs/java-rest/high-level/indices/clone_index.asciidoc new file mode 100644 index 0000000000000..7448b8a402bb0 --- /dev/null +++ b/docs/java-rest/high-level/indices/clone_index.asciidoc @@ -0,0 +1,80 @@ +-- +:api: clone-index +:request: ResizeRequest +:response: ResizeResponse +-- + +[id="{upid}-{api}"] +=== Clone Index API + +[id="{upid}-{api}-request"] +==== Resize Request + +The Clone Index API requires a +{request}+ instance. +A +{request}+ requires two string arguments: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- +<1> The target index (first argument) to clone the source index (second argument) into +<2> The resize type needs to be set to `CLONE` + +==== Optional arguments +The following arguments can optionally be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-timeout] +-------------------------------------------------- +<1> Timeout to wait for the all the nodes to acknowledge the index is opened +as a `TimeValue` +<2> Timeout to wait for the all the nodes to acknowledge the index is opened +as a `String` + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-masterTimeout] +-------------------------------------------------- +<1> Timeout to connect to the master node as a `TimeValue` +<2> Timeout to connect to the master node as a `String` + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-waitForActiveShards] +-------------------------------------------------- +<1> The number of active shard copies to wait for before the clone index API +returns a response, as an `int` +<2> The number of active shard copies to wait for before the clone index API +returns a response, as an `ActiveShardCount` + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-settings] +-------------------------------------------------- +<1> The settings to apply to the target index, which optionally include the +number of shards to create for it + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request-aliases] +-------------------------------------------------- +<1> The aliases to associate the target index with + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Clone Index Response + +The returned +{response}+ allows to retrieve information about the +executed operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> Indicates whether all of the nodes have acknowledged the request +<2> Indicates whether the requisite number of shard copies were started for +each shard in the index before timing out + + diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 0aecc5f0021fc..d07f43abac5f1 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -99,6 +99,7 @@ Index Management:: * <<{upid}-close-index>> * <<{upid}-shrink-index>> * <<{upid}-split-index>> +* <<{upid}-clone-index>> * <<{upid}-refresh>> * <<{upid}-flush>> * <<{upid}-flush-synced>> @@ -133,6 +134,7 @@ include::indices/open_index.asciidoc[] include::indices/close_index.asciidoc[] include::indices/shrink_index.asciidoc[] include::indices/split_index.asciidoc[] +include::indices/clone_index.asciidoc[] include::indices/refresh.asciidoc[] include::indices/flush.asciidoc[] include::indices/flush_synced.asciidoc[] diff --git a/docs/reference/indices.asciidoc b/docs/reference/indices.asciidoc index 2a4cff93ba759..b3c9166437073 100644 --- a/docs/reference/indices.asciidoc +++ b/docs/reference/indices.asciidoc @@ -15,6 +15,7 @@ index settings, aliases, mappings, and index templates. * <> * <> * <> +* <> * <> * <> * <> @@ -72,6 +73,8 @@ include::indices/shrink-index.asciidoc[] include::indices/split-index.asciidoc[] +include::indices/clone-index.asciidoc[] + include::indices/rollover-index.asciidoc[] include::indices/apis/freeze.asciidoc[] diff --git a/docs/reference/indices/clone-index.asciidoc b/docs/reference/indices/clone-index.asciidoc new file mode 100644 index 0000000000000..a57e0e30593ab --- /dev/null +++ b/docs/reference/indices/clone-index.asciidoc @@ -0,0 +1,138 @@ +[[indices-clone-index]] +== Clone Index + +The clone index API allows you to clone an existing index into a new index, +where each original primary shard is cloned into a new primary shard in +the new index. + +[float] +=== How does cloning work? + +Cloning works as follows: + +* First, it creates a new target index with the same definition as the source + index. + +* Then it hard-links segments from the source index into the target index. (If + the file system doesn't support hard-linking, then all segments are copied + into the new index, which is a much more time consuming process.) + +* Finally, it recovers the target index as though it were a closed index which + had just been re-opened. + +[float] +=== Preparing an index for cloning + +Create a new index: + +[source,js] +-------------------------------------------------- +PUT my_source_index +{ + "settings": { + "index.number_of_shards" : 5 + } +} +-------------------------------------------------- +// CONSOLE + +In order to clone an index, the index must be marked as read-only, +and have <> `green`. + +This can be achieved with the following request: + +[source,js] +-------------------------------------------------- +PUT /my_source_index/_settings +{ + "settings": { + "index.blocks.write": true <1> + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +<1> Prevents write operations to this index while still allowing metadata + changes like deleting the index. + +[float] +=== Cloning an index + +To clone `my_source_index` into a new index called `my_target_index`, issue +the following request: + +[source,js] +-------------------------------------------------- +POST my_source_index/_clone/my_target_index +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +The above request returns immediately once the target index has been added to +the cluster state -- it doesn't wait for the clone operation to start. + +[IMPORTANT] +===================================== + +Indices can only be cloned if they satisfy the following requirements: + +* the target index must not exist + +* The source index must have the same number of primary shards as the target index. + +* The node handling the clone process must have sufficient free disk space to + accommodate a second copy of the existing index. + +===================================== + +The `_clone` API is similar to the <> +and accepts `settings` and `aliases` parameters for the target index: + +[source,js] +-------------------------------------------------- +POST my_source_index/_clone/my_target_index +{ + "settings": { + "index.number_of_shards": 5 <1> + }, + "aliases": { + "my_search_indices": {} + } +} +-------------------------------------------------- +// CONSOLE +// TEST[s/^/PUT my_source_index\n{"settings": {"index.blocks.write": true, "index.number_of_shards": "5"}}\n/] + +<1> The number of shards in the target index. This must be equal to the + number of shards in the source index. + + +NOTE: Mappings may not be specified in the `_clone` request. The mappings of +the source index will be used for the target index. + +[float] +=== Monitoring the clone process + +The clone process can be monitored with the <>, or the <> can be used to wait +until all primary shards have been allocated by setting the `wait_for_status` +parameter to `yellow`. + +The `_clone` API returns as soon as the target index has been added to the +cluster state, before any shards have been allocated. At this point, all +shards are in the state `unassigned`. If, for any reason, the target index +can't be allocated, its primary shard will remain `unassigned` until it +can be allocated on that node. + +Once the primary shard is allocated, it moves to state `initializing`, and the +clone process begins. When the clone operation completes, the shard will +become `active`. At that point, Elasticsearch will try to allocate any +replicas and may decide to relocate the primary shard to another node. + +[float] +=== Wait For Active Shards + +Because the clone operation creates a new index to clone the shards to, +the <> setting +on index creation applies to the clone index action as well. diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.clone.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.clone.json new file mode 100644 index 0000000000000..fe847488c2c33 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.clone.json @@ -0,0 +1,39 @@ +{ + "indices.clone": { + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/indices-clone-index.html", + "stability": "stable", + "methods": ["PUT", "POST"], + "url": { + "paths": ["/{index}/_clone/{target}"], + "parts": { + "index": { + "type" : "string", + "required" : true, + "description" : "The name of the source index to clone" + }, + "target": { + "type" : "string", + "required" : true, + "description" : "The name of the target index to clone into" + } + }, + "params": { + "timeout": { + "type" : "time", + "description" : "Explicit operation timeout" + }, + "master_timeout": { + "type" : "time", + "description" : "Specify timeout for connection to master" + }, + "wait_for_active_shards": { + "type" : "string", + "description" : "Set the number of active shards to wait for on the cloned index before the operation returns." + } + } + }, + "body": { + "description" : "The configuration for the target index (`settings` and `aliases`)" + } + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/10_basic.yml new file mode 100644 index 0000000000000..2d87acf63154c --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/10_basic.yml @@ -0,0 +1,111 @@ +--- +setup: + - do: + indices.create: + index: source + wait_for_active_shards: 1 + body: + settings: + index.number_of_shards: 2 + index.number_of_replicas: 0 + - do: + index: + index: source + id: "1" + body: { "foo": "hello world" } + + - do: + index: + index: source + id: "2" + body: { "foo": "hello world 2" } + + - do: + index: + index: source + id: "3" + body: { "foo": "hello world 3" } + +--- +"Clone index via API": + - skip: + version: " - 7.99.99" + reason: index cloning was added in 8.0.0 + # make it read-only + - do: + indices.put_settings: + index: source + body: + index.blocks.write: true + index.number_of_replicas: 0 + + - do: + cluster.health: + wait_for_status: green + index: source + + # now we do the actual clone + - do: + indices.clone: + index: "source" + target: "target" + wait_for_active_shards: 1 + master_timeout: 10s + body: + settings: + index.number_of_replicas: 0 + index.number_of_shards: 2 + + - do: + cluster.health: + wait_for_status: green + + - do: + get: + index: target + id: "1" + + - match: { _index: target } + - match: { _type: _doc } + - match: { _id: "1" } + - match: { _source: { foo: "hello world" } } + + + - do: + get: + index: target + id: "2" + + - match: { _index: target } + - match: { _type: _doc } + - match: { _id: "2" } + - match: { _source: { foo: "hello world 2" } } + + + - do: + get: + index: target + id: "3" + + - match: { _index: target } + - match: { _type: _doc } + - match: { _id: "3" } + - match: { _source: { foo: "hello world 3" } } + +--- +"Create illegal clone indices": + - skip: + version: " - 7.99.99" + reason: index cloning was added in 8.0.0 + # try to do an illegal clone with illegal number_of_shards + - do: + catch: /illegal_argument_exception/ + indices.clone: + index: "source" + target: "target" + wait_for_active_shards: 1 + master_timeout: 10s + body: + settings: + index.number_of_replicas: 0 + index.number_of_shards: 6 diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/20_source_mapping.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/20_source_mapping.yml new file mode 100644 index 0000000000000..a8a4a71de86b1 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/20_source_mapping.yml @@ -0,0 +1,65 @@ +--- +"Clone index ignores target template mapping": + - skip: + version: " - 7.99.99" + reason: index cloning was added in 8.0.0 + # create index + - do: + indices.create: + index: source + wait_for_active_shards: 1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + count: + type: text + + # index document + - do: + index: + index: source + id: "1" + body: { "count": "1" } + + # create template matching shrink target + - do: + indices.put_template: + name: tpl1 + body: + index_patterns: targ* + mappings: + properties: + count: + type: integer + + # make it read-only + - do: + indices.put_settings: + index: source + body: + index.blocks.write: true + index.number_of_replicas: 0 + + - do: + cluster.health: + wait_for_status: green + index: source + + # now we do the actual clone + - do: + indices.clone: + index: "source" + target: "target" + wait_for_active_shards: 1 + master_timeout: 10s + body: + settings: + index.number_of_shards: 1 + index.number_of_replicas: 0 + + - do: + cluster.health: + wait_for_status: green diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/30_copy_settings.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/30_copy_settings.yml new file mode 100644 index 0000000000000..426e67385b331 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.clone/30_copy_settings.yml @@ -0,0 +1,61 @@ +--- +"Copy settings during clone index": + - skip: + version: " - 7.99.99" + reason: index cloning was added in 8.0.0 + features: [arbitrary_key] + + - do: + nodes.info: + node_id: data:true + - set: + nodes._arbitrary_key_: node_id + + - do: + indices.create: + index: source + wait_for_active_shards: 1 + body: + settings: + index.number_of_replicas: 0 + index.number_of_shards: 1 + index.merge.scheduler.max_merge_count: 4 + + # make it read-only + - do: + indices.put_settings: + index: source + body: + index.blocks.write: true + index.number_of_replicas: 0 + + - do: + cluster.health: + wait_for_status: green + index: source + + # now we do an actual clone and copy settings + - do: + indices.clone: + index: "source" + target: "copy-settings-target" + wait_for_active_shards: 1 + master_timeout: 10s + body: + settings: + index.number_of_replicas: 0 + index.number_of_shards: 1 + index.merge.scheduler.max_thread_count: 2 + + - do: + cluster.health: + wait_for_status: green + + - do: + indices.get_settings: + index: "copy-settings-target" + + # settings should be copied + - match: { copy-settings-target.settings.index.merge.scheduler.max_merge_count: "4" } + - match: { copy-settings-target.settings.index.merge.scheduler.max_thread_count: "2" } + - match: { copy-settings-target.settings.index.blocks.write: "true" } diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 9c2ddf2b1d0d5..2b295af52709d 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -598,6 +598,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestCreateIndexAction(settings, restController)); registerHandler.accept(new RestResizeHandler.RestShrinkIndexAction(settings, restController)); registerHandler.accept(new RestResizeHandler.RestSplitIndexAction(settings, restController)); + registerHandler.accept(new RestResizeHandler.RestCloneIndexAction(settings, restController)); registerHandler.accept(new RestRolloverIndexAction(settings, restController)); registerHandler.accept(new RestDeleteIndexAction(settings, restController)); registerHandler.accept(new RestCloseIndexAction(settings, restController)); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequest.java index a42ed270f84f6..1732d1c1df6e0 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequest.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.action.admin.indices.shrink; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.indices.alias.Alias; @@ -100,6 +101,9 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); targetIndexRequest.writeTo(out); out.writeString(sourceIndex); + if (type == ResizeType.CLONE && out.getVersion().before(Version.V_8_0_0)) { + throw new IllegalArgumentException("can't send clone request to a node that's older than " + Version.V_8_0_0); + } out.writeEnum(type); out.writeOptionalBoolean(copySettings); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeType.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeType.java index bca386a9567d6..ccb1c37a02191 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeType.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/ResizeType.java @@ -23,5 +23,5 @@ * The type of the resize operation */ public enum ResizeType { - SHRINK, SPLIT; + SHRINK, SPLIT, CLONE; } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java index a64b6e65d72d1..f13c1096d5548 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java @@ -133,8 +133,13 @@ static CreateIndexClusterStateUpdateRequest prepareCreateIndexRequest(final Resi if (IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.exists(targetIndexSettings)) { numShards = IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings); } else { - assert resizeRequest.getResizeType() == ResizeType.SHRINK : "split must specify the number of shards explicitly"; - numShards = 1; + assert resizeRequest.getResizeType() != ResizeType.SPLIT : "split must specify the number of shards explicitly"; + if (resizeRequest.getResizeType() == ResizeType.SHRINK) { + numShards = 1; + } else { + assert resizeRequest.getResizeType() == ResizeType.CLONE; + numShards = metaData.getNumberOfShards(); + } } for (int i = 0; i < numShards; i++) { @@ -151,15 +156,17 @@ static CreateIndexClusterStateUpdateRequest prepareCreateIndexRequest(final Resi + "] docs - too many documents in shards " + shardIds); } } - } else { + } else if (resizeRequest.getResizeType() == ResizeType.SPLIT) { Objects.requireNonNull(IndexMetaData.selectSplitShard(i, metaData, numShards)); // we just execute this to ensure we get the right exceptions if the number of shards is wrong or less then etc. + } else { + Objects.requireNonNull(IndexMetaData.selectCloneShard(i, metaData, numShards)); + // we just execute this to ensure we get the right exceptions if the number of shards is wrong etc. } } if (IndexMetaData.INDEX_ROUTING_PARTITION_SIZE_SETTING.exists(targetIndexSettings)) { throw new IllegalArgumentException("cannot provide a routing partition size value when resizing an index"); - } if (IndexMetaData.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.exists(targetIndexSettings)) { // if we have a source index with 1 shards it's legal to set this diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index 3598753f80d9c..6bd98926865e9 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -1500,6 +1500,22 @@ public static ShardId selectSplitShard(int shardId, IndexMetaData sourceIndexMet return new ShardId(sourceIndexMetadata.getIndex(), shardId/routingFactor); } + /** + * Returns the source shard ID to clone the given target shard off + * @param shardId the id of the target shard to clone into + * @param sourceIndexMetadata the source index metadata + * @param numTargetShards the total number of shards in the target index + * @return a the source shard ID to clone from + */ + public static ShardId selectCloneShard(int shardId, IndexMetaData sourceIndexMetadata, int numTargetShards) { + int numSourceShards = sourceIndexMetadata.getNumberOfShards(); + if (numSourceShards != numTargetShards) { + throw new IllegalArgumentException("the number of target shards (" + numTargetShards + ") must be the same as the number of " + + " source shards ( " + numSourceShards + ")"); + } + return new ShardId(sourceIndexMetadata.getIndex(), shardId); + } + private static void assertSplitMetadata(int numSourceShards, int numTargetShards, IndexMetaData sourceIndexMetadata) { if (numSourceShards > numTargetShards) { throw new IllegalArgumentException("the number of source shards [" + numSourceShards @@ -1530,8 +1546,9 @@ public static Set selectRecoverFromShards(int shardId, IndexMetaData so return selectShrinkShards(shardId, sourceIndexMetadata, numTargetShards); } else if (sourceIndexMetadata.getNumberOfShards() < numTargetShards) { return Collections.singleton(selectSplitShard(shardId, sourceIndexMetadata, numTargetShards)); + } else { + return Collections.singleton(selectCloneShard(shardId, sourceIndexMetadata, numTargetShards)); } - throw new IllegalArgumentException("can't select recover from shards if both indices have the same number of shards"); } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index 863871314060e..fc97c80a65ed5 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -705,9 +705,16 @@ static void validateSplitIndex(ClusterState state, String sourceIndex, IndexMetaData.selectSplitShard(0, sourceMetaData, IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); } - private static IndexMetaData validateResize(ClusterState state, String sourceIndex, - Set targetIndexMappingsTypes, String targetIndexName, - Settings targetIndexSettings) { + static void validateCloneIndex(ClusterState state, String sourceIndex, + Set targetIndexMappingsTypes, String targetIndexName, + Settings targetIndexSettings) { + IndexMetaData sourceMetaData = validateResize(state, sourceIndex, targetIndexMappingsTypes, targetIndexName, targetIndexSettings); + IndexMetaData.selectCloneShard(0, sourceMetaData, IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); + } + + static IndexMetaData validateResize(ClusterState state, String sourceIndex, + Set targetIndexMappingsTypes, String targetIndexName, + Settings targetIndexSettings) { if (state.metaData().hasIndex(targetIndexName)) { throw new ResourceAlreadyExistsException(state.metaData().index(targetIndexName).getIndex()); } @@ -760,6 +767,9 @@ static void prepareResizeIndexSettings( } else if (type == ResizeType.SPLIT) { validateSplitIndex(currentState, resizeSourceIndex.getName(), mappingKeys, resizeIntoName, indexSettingsBuilder.build()); indexSettingsBuilder.putNull(initialRecoveryIdFilter); + } else if (type == ResizeType.CLONE) { + validateCloneIndex(currentState, resizeSourceIndex.getName(), mappingKeys, resizeIntoName, indexSettingsBuilder.build()); + indexSettingsBuilder.putNull(initialRecoveryIdFilter); } else { throw new IllegalStateException("unknown resize type is " + type); } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/ResizeAllocationDecider.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/ResizeAllocationDecider.java index ca1c52addac3f..5dc51e3143463 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/ResizeAllocationDecider.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/ResizeAllocationDecider.java @@ -53,11 +53,13 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, Routing } IndexMetaData sourceIndexMetaData = allocation.metaData().getIndexSafe(resizeSourceIndex); if (indexMetaData.getNumberOfShards() < sourceIndexMetaData.getNumberOfShards()) { - // this only handles splits so far. + // this only handles splits and clone so far. return Decision.ALWAYS; } - ShardId shardId = IndexMetaData.selectSplitShard(shardRouting.id(), sourceIndexMetaData, indexMetaData.getNumberOfShards()); + ShardId shardId = indexMetaData.getNumberOfShards() == sourceIndexMetaData.getNumberOfShards() ? + IndexMetaData.selectCloneShard(shardRouting.id(), sourceIndexMetaData, indexMetaData.getNumberOfShards()) : + IndexMetaData.selectSplitShard(shardRouting.id(), sourceIndexMetaData, indexMetaData.getNumberOfShards()); ShardRouting sourceShardRouting = allocation.routingNodes().activePrimary(shardId); if (sourceShardRouting == null) { return allocation.decision(Decision.NO, NAME, "source primary shard [%s] is not active", shardId); diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java index 3d0158cf95f0f..4575d88496f29 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java @@ -93,4 +93,24 @@ protected ResizeType getResizeType() { } + public static class RestCloneIndexAction extends RestResizeHandler { + + public RestCloneIndexAction(final Settings settings, final RestController controller) { + super(settings); + controller.registerHandler(RestRequest.Method.PUT, "/{index}/_clone/{target}", this); + controller.registerHandler(RestRequest.Method.POST, "/{index}/_clone/{target}", this); + } + + @Override + public String getName() { + return "clone_index_action"; + } + + @Override + protected ResizeType getResizeType() { + return ResizeType.CLONE; + } + + } + } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java new file mode 100644 index 0000000000000..73d6bede61650 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java @@ -0,0 +1,126 @@ +/* + * 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.action.admin.indices.create; + +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse; +import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.index.seqno.SeqNoStats; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.VersionUtils; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; +import static org.hamcrest.Matchers.equalTo; + +public class CloneIndexIT extends ESIntegTestCase { + + @Override + protected boolean forbidPrivateIndexSettings() { + return false; + } + + public void testCreateCloneIndex() { + Version version = VersionUtils.randomIndexCompatibleVersion(random()); + int numPrimaryShards = randomIntBetween(1, 5); + prepareCreate("source").setSettings(Settings.builder().put(indexSettings()) + .put("number_of_shards", numPrimaryShards) + .put("index.version.created", version) + ).get(); + final int docs = randomIntBetween(0, 128); + for (int i = 0; i < docs; i++) { + client().prepareIndex("source", "type") + .setSource("{\"foo\" : \"bar\", \"i\" : " + i + "}", XContentType.JSON).get(); + } + internalCluster().ensureAtLeastNumDataNodes(2); + // ensure all shards are allocated otherwise the ensure green below might not succeed since we require the merge node + // if we change the setting too quickly we will end up with one replica unassigned which can't be assigned anymore due + // to the require._name below. + ensureGreen(); + // relocate all shards to one node such that we can merge it. + client().admin().indices().prepareUpdateSettings("source") + .setSettings(Settings.builder() + .put("index.blocks.write", true)).get(); + ensureGreen(); + + final IndicesStatsResponse sourceStats = client().admin().indices().prepareStats("source").setSegments(true).get(); + + // disable rebalancing to be able to capture the right stats. balancing can move the target primary + // making it hard to pin point the source shards. + client().admin().cluster().prepareUpdateSettings().setTransientSettings(Settings.builder().put( + EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), "none" + )).get(); + try { + + final boolean createWithReplicas = randomBoolean(); + assertAcked(client().admin().indices().prepareResizeIndex("source", "target") + .setResizeType(ResizeType.CLONE) + .setSettings(Settings.builder() + .put("index.number_of_replicas", createWithReplicas ? 1 : 0) + .putNull("index.blocks.write") + .build()).get()); + ensureGreen(); + + final IndicesStatsResponse targetStats = client().admin().indices().prepareStats("target").get(); + assertThat(targetStats.getIndex("target").getIndexShards().keySet().size(), equalTo(numPrimaryShards)); + + for (int i = 0; i < numPrimaryShards; i++) { + final SeqNoStats sourceSeqNoStats = sourceStats.getIndex("source").getIndexShards().get(i).getAt(0).getSeqNoStats(); + final SeqNoStats targetSeqNoStats = targetStats.getIndex("target").getIndexShards().get(i).getAt(0).getSeqNoStats(); + assertEquals(sourceSeqNoStats.getMaxSeqNo(), targetSeqNoStats.getMaxSeqNo()); + assertEquals(targetSeqNoStats.getMaxSeqNo(), targetSeqNoStats.getLocalCheckpoint()); + } + + final int size = docs > 0 ? 2 * docs : 1; + assertHitCount(client().prepareSearch("target").setSize(size).setQuery(new TermsQueryBuilder("foo", "bar")).get(), docs); + + if (createWithReplicas == false) { + // bump replicas + client().admin().indices().prepareUpdateSettings("target") + .setSettings(Settings.builder() + .put("index.number_of_replicas", 1)).get(); + ensureGreen(); + assertHitCount(client().prepareSearch("target").setSize(size).setQuery(new TermsQueryBuilder("foo", "bar")).get(), docs); + } + + for (int i = docs; i < 2 * docs; i++) { + client().prepareIndex("target", "type") + .setSource("{\"foo\" : \"bar\", \"i\" : " + i + "}", XContentType.JSON).get(); + } + flushAndRefresh(); + assertHitCount(client().prepareSearch("target").setSize(2 * size).setQuery(new TermsQueryBuilder("foo", "bar")).get(), + 2 * docs); + assertHitCount(client().prepareSearch("source").setSize(size).setQuery(new TermsQueryBuilder("foo", "bar")).get(), docs); + GetSettingsResponse target = client().admin().indices().prepareGetSettings("target").get(); + assertEquals(version, target.getIndexToSettings().get("target").getAsVersion("index.version.created", null)); + } finally { + // clean up + client().admin().cluster().prepareUpdateSettings().setTransientSettings(Settings.builder().put( + EnableAllocationDecider.CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), (String)null + )).get(); + } + + } + +} diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java index e3b6234daa7ca..54a97871ec696 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java @@ -203,8 +203,7 @@ public void testSelectResizeShards() { assertEquals(IndexMetaData.selectShrinkShards(shard, shrink, numTargetShards), IndexMetaData.selectRecoverFromShards(shard, shrink, numTargetShards)); - assertEquals("can't select recover from shards if both indices have the same number of shards", - expectThrows(IllegalArgumentException.class, () -> IndexMetaData.selectRecoverFromShards(0, shrink, 32)).getMessage()); + IndexMetaData.selectRecoverFromShards(0, shrink, 32); } public void testSelectSplitShard() {