|
24 | 24 | import org.elasticsearch.client.Response;
|
25 | 25 | import org.elasticsearch.client.ResponseException;
|
26 | 26 | import org.elasticsearch.cluster.metadata.IndexMetaData;
|
| 27 | +import org.elasticsearch.cluster.metadata.MetaDataIndexStateService; |
| 28 | +import org.elasticsearch.common.Booleans; |
27 | 29 | import org.elasticsearch.common.settings.Settings;
|
28 | 30 | import org.elasticsearch.common.util.concurrent.AbstractRunnable;
|
| 31 | +import org.elasticsearch.common.xcontent.support.XContentMapValues; |
29 | 32 | import org.elasticsearch.index.IndexSettings;
|
30 | 33 | import org.elasticsearch.rest.action.document.RestIndexAction;
|
31 | 34 | import org.elasticsearch.test.rest.yaml.ObjectPath;
|
32 | 35 |
|
33 | 36 | import java.io.IOException;
|
34 | 37 | import java.util.ArrayList;
|
| 38 | +import java.util.Collection; |
35 | 39 | import java.util.List;
|
| 40 | +import java.util.Locale; |
36 | 41 | import java.util.Map;
|
37 | 42 | import java.util.concurrent.Future;
|
38 | 43 | import java.util.function.Predicate;
|
|
43 | 48 | import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY;
|
44 | 49 | import static org.hamcrest.Matchers.equalTo;
|
45 | 50 | import static org.hamcrest.Matchers.hasSize;
|
| 51 | +import static org.hamcrest.Matchers.is; |
46 | 52 | import static org.hamcrest.Matchers.notNullValue;
|
| 53 | +import static org.hamcrest.Matchers.nullValue; |
47 | 54 |
|
48 | 55 | /**
|
49 | 56 | * In depth testing of the recovery mechanism during a rolling restart.
|
@@ -310,4 +317,144 @@ public void testRecoveryWithSoftDeletes() throws Exception {
|
310 | 317 | }
|
311 | 318 | ensureGreen(index);
|
312 | 319 | }
|
| 320 | + |
| 321 | + /** |
| 322 | + * This test creates an index in the non upgraded cluster and closes it. It then checks that the index |
| 323 | + * is effectively closed and potentially replicated (if the version the index was created on supports |
| 324 | + * the replication of closed indices) during the rolling upgrade. |
| 325 | + */ |
| 326 | + public void testRecoveryClosedIndex() throws Exception { |
| 327 | + final String indexName = "closed_index_created_on_old"; |
| 328 | + if (CLUSTER_TYPE == ClusterType.OLD) { |
| 329 | + createIndex(indexName, Settings.builder() |
| 330 | + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) |
| 331 | + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 1) |
| 332 | + // if the node with the replica is the first to be restarted, while a replica is still recovering |
| 333 | + // then delayed allocation will kick in. When the node comes back, the master will search for a copy |
| 334 | + // but the recovering copy will be seen as invalid and the cluster health won't return to GREEN |
| 335 | + // before timing out |
| 336 | + .put(INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), "100ms") |
| 337 | + .put(SETTING_ALLOCATION_MAX_RETRY.getKey(), "0") // fail faster |
| 338 | + .build()); |
| 339 | + ensureGreen(indexName); |
| 340 | + closeIndex(indexName); |
| 341 | + } |
| 342 | + |
| 343 | + final Version indexVersionCreated = indexVersionCreated(indexName); |
| 344 | + if (indexVersionCreated.onOrAfter(Version.V_8_0_0)) { |
| 345 | + // index was created on a version that supports the replication of closed indices, |
| 346 | + // so we expect the index to be closed and replicated |
| 347 | + ensureGreen(indexName); |
| 348 | + assertClosedIndex(indexName, true); |
| 349 | + } else { |
| 350 | + assertClosedIndex(indexName, false); |
| 351 | + } |
| 352 | + } |
| 353 | + |
| 354 | + /** |
| 355 | + * This test creates and closes a new index at every stage of the rolling upgrade. It then checks that the index |
| 356 | + * is effectively closed and potentially replicated if the cluster supports replication of closed indices at the |
| 357 | + * time the index was closed. |
| 358 | + */ |
| 359 | + public void testCloseIndexDuringRollingUpgrade() throws Exception { |
| 360 | + final Version minimumNodeVersion = minimumNodeVersion(); |
| 361 | + final String indexName = |
| 362 | + String.join("_", "index", CLUSTER_TYPE.toString(), Integer.toString(minimumNodeVersion.id)).toLowerCase(Locale.ROOT); |
| 363 | + |
| 364 | + if (indexExists(indexName) == false) { |
| 365 | + createIndex(indexName, Settings.builder() |
| 366 | + .put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1) |
| 367 | + .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0) |
| 368 | + .build()); |
| 369 | + ensureGreen(indexName); |
| 370 | + closeIndex(indexName); |
| 371 | + } |
| 372 | + |
| 373 | + if (minimumNodeVersion.onOrAfter(Version.V_8_0_0)) { |
| 374 | + // index is created on a version that supports the replication of closed indices, |
| 375 | + // so we expect the index to be closed and replicated |
| 376 | + ensureGreen(indexName); |
| 377 | + assertClosedIndex(indexName, true); |
| 378 | + } else { |
| 379 | + assertClosedIndex(indexName, false); |
| 380 | + } |
| 381 | + } |
| 382 | + |
| 383 | + /** |
| 384 | + * Returns the version in which the given index has been created |
| 385 | + */ |
| 386 | + private static Version indexVersionCreated(final String indexName) throws IOException { |
| 387 | + final Request request = new Request("GET", "/" + indexName + "/_settings"); |
| 388 | + final String versionCreatedSetting = indexName + ".settings.index.version.created"; |
| 389 | + request.addParameter("filter_path", versionCreatedSetting); |
| 390 | + |
| 391 | + final Response response = client().performRequest(request); |
| 392 | + return Version.fromId(Integer.parseInt(ObjectPath.createFromResponse(response).evaluate(versionCreatedSetting))); |
| 393 | + } |
| 394 | + |
| 395 | + /** |
| 396 | + * Returns the minimum node version among all nodes of the cluster |
| 397 | + */ |
| 398 | + private static Version minimumNodeVersion() throws IOException { |
| 399 | + final Request request = new Request("GET", "_nodes"); |
| 400 | + request.addParameter("filter_path", "nodes.*.version"); |
| 401 | + |
| 402 | + final Response response = client().performRequest(request); |
| 403 | + final Map<String, Object> nodes = ObjectPath.createFromResponse(response).evaluate("nodes"); |
| 404 | + |
| 405 | + Version minVersion = null; |
| 406 | + for (Map.Entry<String, Object> node : nodes.entrySet()) { |
| 407 | + @SuppressWarnings("unchecked") |
| 408 | + Version nodeVersion = Version.fromString((String) ((Map<String, Object>) node.getValue()).get("version")); |
| 409 | + if (minVersion == null || minVersion.after(nodeVersion)) { |
| 410 | + minVersion = nodeVersion; |
| 411 | + } |
| 412 | + } |
| 413 | + assertNotNull(minVersion); |
| 414 | + return minVersion; |
| 415 | + } |
| 416 | + |
| 417 | + /** |
| 418 | + * Asserts that an index is closed in the cluster state. If `checkRoutingTable` is true, it also asserts |
| 419 | + * that the index has started shards. |
| 420 | + */ |
| 421 | + @SuppressWarnings("unchecked") |
| 422 | + private void assertClosedIndex(final String index, final boolean checkRoutingTable) throws IOException { |
| 423 | + final Map<String, ?> state = entityAsMap(client().performRequest(new Request("GET", "/_cluster/state"))); |
| 424 | + |
| 425 | + final Map<String, ?> metadata = (Map<String, Object>) XContentMapValues.extractValue("metadata.indices." + index, state); |
| 426 | + assertThat(metadata, notNullValue()); |
| 427 | + assertThat(metadata.get("state"), equalTo("close")); |
| 428 | + |
| 429 | + final Map<String, ?> blocks = (Map<String, Object>) XContentMapValues.extractValue("blocks.indices." + index, state); |
| 430 | + assertThat(blocks, notNullValue()); |
| 431 | + assertThat(blocks.containsKey(String.valueOf(MetaDataIndexStateService.INDEX_CLOSED_BLOCK_ID)), is(true)); |
| 432 | + |
| 433 | + final Map<String, ?> settings = (Map<String, Object>) XContentMapValues.extractValue("settings", metadata); |
| 434 | + assertThat(settings, notNullValue()); |
| 435 | + |
| 436 | + final int numberOfShards = Integer.parseInt((String) XContentMapValues.extractValue("index.number_of_shards", settings)); |
| 437 | + final int numberOfReplicas = Integer.parseInt((String) XContentMapValues.extractValue("index.number_of_replicas", settings)); |
| 438 | + |
| 439 | + final Map<String, ?> routingTable = (Map<String, Object>) XContentMapValues.extractValue("routing_table.indices." + index, state); |
| 440 | + if (checkRoutingTable) { |
| 441 | + assertThat(routingTable, notNullValue()); |
| 442 | + assertThat(Booleans.parseBoolean((String) XContentMapValues.extractValue("index.verified_before_close", settings)), is(true)); |
| 443 | + |
| 444 | + for (int i = 0; i < numberOfShards; i++) { |
| 445 | + final Collection<Map<String, ?>> shards = |
| 446 | + (Collection<Map<String, ?>>) XContentMapValues.extractValue("shards." + i, routingTable); |
| 447 | + assertThat(shards, notNullValue()); |
| 448 | + assertThat(shards.size(), equalTo(numberOfReplicas + 1)); |
| 449 | + for (Map<String, ?> shard : shards) { |
| 450 | + assertThat(XContentMapValues.extractValue("shard", shard), equalTo(i)); |
| 451 | + assertThat(XContentMapValues.extractValue("state", shard), equalTo("STARTED")); |
| 452 | + assertThat(XContentMapValues.extractValue("index", shard), equalTo(index)); |
| 453 | + } |
| 454 | + } |
| 455 | + } else { |
| 456 | + assertThat(routingTable, nullValue()); |
| 457 | + assertThat(XContentMapValues.extractValue("index.verified_before_close", settings), nullValue()); |
| 458 | + } |
| 459 | + } |
313 | 460 | }
|
0 commit comments