Skip to content

Commit

Permalink
Avoid regular indices in frozen tier (elastic#70141) (elastic#70313)
Browse files Browse the repository at this point in the history
The frozen tier will be dedicated for partially cached searchable
snapshots. This PR ensures that we do not allow allocating regular
indices (including fully cached searchable snapshots) to the frozen
tier.
  • Loading branch information
henningandersen authored Mar 11, 2021
1 parent 366e2f4 commit 3baaed2
Show file tree
Hide file tree
Showing 18 changed files with 316 additions and 44 deletions.
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ tasks.register("verifyVersions") {
* after the backport of the backcompat code is complete.
*/

boolean bwc_tests_enabled = true
String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
boolean bwc_tests_enabled = false
// place a PR link here when committing bwc changes:
String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/70141"
/*
* FIPS 140-2 behavior was fixed in 7.11.0. Before that there is no way to run elasticsearch in a
* JVM that is properly configured to be in fips mode with BCFIPS. For now we need to disable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.cluster.routing.allocation;

import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
import org.elasticsearch.action.admin.indices.shrink.ResizeType;
import org.elasticsearch.action.admin.indices.template.put.PutComposableIndexTemplateAction;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
Expand Down Expand Up @@ -271,6 +272,35 @@ public void testTierFilteringIgnoredByFilterAllocationDecider() {
.get();
}

public void testIllegalOnFrozen() {
startDataNode();

IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> createIndex(index, Settings.builder()
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_FROZEN)
.build()));
assertThat(e.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));

String initialTier = randomFrom(DataTier.DATA_HOT, DataTier.DATA_WARM, DataTier.DATA_COLD);
createIndex(index, Settings.builder()
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, initialTier)
.build());

IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, () -> updatePreference(DataTier.DATA_FROZEN));
assertThat(e2.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));

updatePreference(randomValueOtherThan(initialTier, () -> randomFrom(DataTier.DATA_HOT, DataTier.DATA_WARM, DataTier.DATA_COLD)));
}

private void updatePreference(String tier) {
client().admin().indices().updateSettings(new UpdateSettingsRequest(index)
.settings(org.elasticsearch.common.collect.Map.of(DataTierAllocationDecider.INDEX_ROUTING_PREFER, tier))).actionGet();
}

private DataTiersFeatureSetUsage getUsage() {
XPackUsageResponse usages = new XPackUsageRequestBuilder(client()).execute().actionGet();
XPackFeatureSet.Usage dtUsage = usages.getUsages().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexModule;
import org.elasticsearch.xpack.core.DataTier;
import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static org.elasticsearch.xpack.core.DataTier.DATA_FROZEN;

/**
* The {@code DataTierAllocationDecider} is a custom allocation decider that behaves similar to the
* {@link org.elasticsearch.cluster.routing.allocation.decider.FilterAllocationDecider}, however it
Expand All @@ -45,20 +52,21 @@ public class DataTierAllocationDecider extends AllocationDecider {
public static final String INDEX_ROUTING_PREFER = "index.routing.allocation.include._tier_preference";
public static final String INDEX_ROUTING_EXCLUDE = "index.routing.allocation.exclude._tier";

private static final DataTierValidator VALIDATOR = new DataTierValidator();
public static final Setting<String> CLUSTER_ROUTING_REQUIRE_SETTING = Setting.simpleString(CLUSTER_ROUTING_REQUIRE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
public static final Setting<String> CLUSTER_ROUTING_INCLUDE_SETTING = Setting.simpleString(CLUSTER_ROUTING_INCLUDE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
public static final Setting<String> CLUSTER_ROUTING_EXCLUDE_SETTING = Setting.simpleString(CLUSTER_ROUTING_EXCLUDE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
public static final Setting<String> INDEX_ROUTING_REQUIRE_SETTING = Setting.simpleString(INDEX_ROUTING_REQUIRE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
public static final Setting<String> INDEX_ROUTING_INCLUDE_SETTING = Setting.simpleString(INDEX_ROUTING_INCLUDE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
public static final Setting<String> INDEX_ROUTING_EXCLUDE_SETTING = Setting.simpleString(INDEX_ROUTING_EXCLUDE,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
public static final Setting<String> INDEX_ROUTING_PREFER_SETTING = Setting.simpleString(INDEX_ROUTING_PREFER,
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);

private static void validateTierSetting(String setting) {
if (Strings.hasText(setting)) {
Expand All @@ -71,6 +79,32 @@ private static void validateTierSetting(String setting) {
}
}

private static class DataTierValidator implements Setting.Validator<String> {
private static final Collection<Setting<?>> dependencies =
org.elasticsearch.common.collect.List.of(IndexModule.INDEX_STORE_TYPE_SETTING,
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING);

@Override
public void validate(String value) {
validateTierSetting(value);
}

@Override
public void validate(String value, Map<Setting<?>, Object> settings) {
if (Strings.hasText(value) && SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(settings) == false) {
String[] split = value.split(",");
if (Arrays.stream(split).anyMatch(DATA_FROZEN::equals)) {
throw new IllegalArgumentException("[" + DATA_FROZEN + "] tier can only be used for partial searchable snapshots");
}
}
}

@Override
public Iterator<Setting<?>> settings() {
return dependencies.iterator();
}
}

private volatile String clusterRequire;
private volatile String clusterInclude;
private volatile String clusterExclude;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,41 @@
package org.elasticsearch.xpack.searchablesnapshots;

import org.elasticsearch.Version;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;

import java.util.Map;

import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING;

public class SearchableSnapshotsConstants {
public static final String SNAPSHOT_DIRECTORY_FACTORY_KEY = "snapshot";

public static final String SNAPSHOT_RECOVERY_STATE_FACTORY_KEY = "snapshot_prewarm";
public static final Setting<Boolean> SNAPSHOT_PARTIAL_SETTING = Setting.boolSetting(
"index.store.snapshot.partial",
false,
Setting.Property.IndexScope,
Setting.Property.PrivateIndex,
Setting.Property.NotCopyableOnResize
);

public static boolean isSearchableSnapshotStore(Settings indexSettings) {
return SNAPSHOT_DIRECTORY_FACTORY_KEY.equals(INDEX_STORE_TYPE_SETTING.get(indexSettings));
}

/**
* Based on a map from setting to value, do the settings represent a partial searchable snapshot index?
*
* Both index.store.type and index.store.snapshot.partial must be supplied.
*/
public static boolean isPartialSearchableSnapshotIndex(Map<Setting<?>, Object> indexSettings) {
assert indexSettings.containsKey(INDEX_STORE_TYPE_SETTING) : "must include store type in map";
assert indexSettings.get(SNAPSHOT_PARTIAL_SETTING) != null : "partial setting must be non-null in map (has default value)";
return SNAPSHOT_DIRECTORY_FACTORY_KEY.equals(indexSettings.get(INDEX_STORE_TYPE_SETTING))
&& (boolean) indexSettings.get(SNAPSHOT_PARTIAL_SETTING);
}

public static final String CACHE_FETCH_ASYNC_THREAD_POOL_NAME = "searchable_snapshots_cache_fetch_async";
public static final String CACHE_FETCH_ASYNC_THREAD_POOL_SETTING = "xpack.searchable_snapshots.cache_fetch_async_thread_pool";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.cluster.routing.allocation;

import joptsimple.internal.Strings;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ESAllocationTestCase;
Expand All @@ -27,20 +28,26 @@
import org.elasticsearch.cluster.routing.allocation.decider.Decision;
import org.elasticsearch.cluster.routing.allocation.decider.ReplicaAfterPrimaryActiveAllocationDecider;
import org.elasticsearch.cluster.routing.allocation.decider.SameShardAllocationDecider;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexModule;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.snapshots.EmptySnapshotsInfoService;
import org.elasticsearch.test.gateway.TestGatewayAllocator;
import org.elasticsearch.xpack.core.DataTier;
import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import static org.elasticsearch.xpack.core.DataTier.DATA_FROZEN;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;

Expand Down Expand Up @@ -702,6 +709,53 @@ public void testExistedClusterFilters() {
"tier filters [data_hot,data_warm]"));
}

public void testFrozenIllegalForRegularIndices() {
List<String> tierList = new ArrayList<>(randomSubsetOf(DataTier.ALL_DATA_TIERS));
if (tierList.contains(DATA_FROZEN) == false) {
tierList.add(DATA_FROZEN);
}
Randomness.shuffle(tierList);

String value = Strings.join(tierList, ",");
Setting<String> setting = randomTierSetting();
Settings.Builder builder = Settings.builder().put(setting.getKey(), value);
if (randomBoolean()) {
builder.put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY);
}

Settings settings = builder.build();
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> setting.get(settings));
assertThat(exception.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));
}

public void testFrozenLegalForPartialSnapshot() {
List<String> tierList = new ArrayList<>(randomSubsetOf(DataTier.ALL_DATA_TIERS));
if (tierList.contains(DATA_FROZEN) == false) {
tierList.add(DATA_FROZEN);
}
Randomness.shuffle(tierList);

String value = Strings.join(tierList, ",");
Setting<String> setting = randomTierSetting();
Settings.Builder builder = Settings.builder().put(setting.getKey(), value);
builder.put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY);
builder.put(SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING.getKey(), true);

Settings settings = builder.build();

// validate do not throw
assertThat(setting.get(settings), equalTo(value));
}

public Setting<String> randomTierSetting() {
//noinspection unchecked
return randomFrom(
DataTierAllocationDecider.INDEX_ROUTING_EXCLUDE_SETTING,
DataTierAllocationDecider.INDEX_ROUTING_INCLUDE_SETTING,
DataTierAllocationDecider.INDEX_ROUTING_REQUIRE_SETTING,
DataTierAllocationDecider.INDEX_ROUTING_PREFER_SETTING);
}

private ClusterState prepareState(ClusterState initialState) {
return prepareState(initialState, Settings.EMPTY);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.searchablesnapshots;

import org.elasticsearch.index.IndexModule;
import org.elasticsearch.test.ESTestCase;

import org.elasticsearch.common.collect.Map;

import static org.hamcrest.Matchers.is;

public class SearchableSnapshotsConstantsTests extends ESTestCase {

public void testIsPartialSearchableSnapshotIndex() {
assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY,
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, false)),
is(false));

assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, "abc",
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, randomBoolean())),
is(false));

assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY,
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, true)),
is(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ protected void mountSnapshot(
storage
);

final RestoreSnapshotResponse restoreResponse = client().execute(MountSearchableSnapshotAction.INSTANCE, mountRequest).get();
final RestoreSnapshotResponse restoreResponse = client().execute(MountSearchableSnapshotAction.INSTANCE, mountRequest).actionGet();
assertThat(restoreResponse.getRestoreInfo().successfulShards(), equalTo(getNumShards(restoredIndexName).numPrimaries));
assertThat(restoreResponse.getRestoreInfo().failedShards(), equalTo(0));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.searchablesnapshots;

import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider;
import org.elasticsearch.xpack.core.DataTier;
import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;

@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST)
public class SearchableSnapshotDataTierIntegTests extends BaseSearchableSnapshotsIntegTestCase {

private static final String repoName = "test-repo";
private static final String indexName = "test-index";
private static final String snapshotName = "test-snapshot";
private static final String mountedIndexName = "test-index-mounted";
private static final Settings frozenSettings = Settings.builder()
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_FROZEN)
.build();

public void testPartialLegalOnFrozen() throws Exception {
createRepository(repoName, "fs");
createIndex(indexName);
createFullSnapshot(repoName, snapshotName);
Settings mountSettings = randomFrom(Settings.EMPTY, frozenSettings);
mountSnapshot(
repoName,
snapshotName,
indexName,
mountedIndexName,
mountSettings,
MountSearchableSnapshotRequest.Storage.SHARED_CACHE
);

updatePreference(DataTier.DATA_FROZEN);
}

public void testFullIllegalOnFrozen() throws Exception {
createRepository(repoName, "fs");
createIndex(indexName);
createFullSnapshot(repoName, snapshotName);
expectThrows(
IllegalArgumentException.class,
() -> mountSnapshot(
repoName,
snapshotName,
indexName,
mountedIndexName,
frozenSettings,
MountSearchableSnapshotRequest.Storage.FULL_COPY
)
);
Settings mountSettings = randomFrom(
Settings.EMPTY,
Settings.builder()
.put(
DataTierAllocationDecider.INDEX_ROUTING_PREFER,
randomValueOtherThan(DataTier.DATA_FROZEN, () -> randomFrom(DataTier.ALL_DATA_TIERS))
)
.build()
);
mountSnapshot(repoName, snapshotName, indexName, mountedIndexName, mountSettings, MountSearchableSnapshotRequest.Storage.FULL_COPY);

expectThrows(IllegalArgumentException.class, () -> updatePreference(DataTier.DATA_FROZEN));
}

private void updatePreference(String tier) {
client().admin()
.indices()
.updateSettings(
new UpdateSettingsRequest(mountedIndexName).settings(
org.elasticsearch.common.collect.Map.of(DataTierAllocationDecider.INDEX_ROUTING_PREFER, tier)
)
)
.actionGet();
}
}
Loading

0 comments on commit 3baaed2

Please sign in to comment.