Skip to content

Commit 85f10ee

Browse files
Avoid regular indices in frozen tier (elastic#70141) (elastic#70313) (elastic#70315)
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.
1 parent e9bebf8 commit 85f10ee

File tree

18 files changed

+315
-43
lines changed

18 files changed

+315
-43
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ tasks.register("verifyVersions") {
192192
*/
193193

194194
boolean bwc_tests_enabled = true
195-
String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
195+
// place a PR link here when committing bwc changes:
196+
String bwc_tests_disabled_issue = ""
196197
/*
197198
* FIPS 140-2 behavior was fixed in 7.11.0. Before that there is no way to run elasticsearch in a
198199
* JVM that is properly configured to be in fips mode with BCFIPS. For now we need to disable

x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierIT.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

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

10+
import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
1011
import org.elasticsearch.action.admin.indices.shrink.ResizeType;
1112
import org.elasticsearch.action.admin.indices.template.put.PutComposableIndexTemplateAction;
1213
import org.elasticsearch.cluster.health.ClusterHealthStatus;
@@ -271,6 +272,35 @@ public void testTierFilteringIgnoredByFilterAllocationDecider() {
271272
.get();
272273
}
273274

275+
public void testIllegalOnFrozen() {
276+
startDataNode();
277+
278+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
279+
() -> createIndex(index, Settings.builder()
280+
.put("index.number_of_shards", 1)
281+
.put("index.number_of_replicas", 0)
282+
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_FROZEN)
283+
.build()));
284+
assertThat(e.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));
285+
286+
String initialTier = randomFrom(DataTier.DATA_HOT, DataTier.DATA_WARM, DataTier.DATA_COLD);
287+
createIndex(index, Settings.builder()
288+
.put("index.number_of_shards", 1)
289+
.put("index.number_of_replicas", 0)
290+
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, initialTier)
291+
.build());
292+
293+
IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, () -> updatePreference(DataTier.DATA_FROZEN));
294+
assertThat(e2.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));
295+
296+
updatePreference(randomValueOtherThan(initialTier, () -> randomFrom(DataTier.DATA_HOT, DataTier.DATA_WARM, DataTier.DATA_COLD)));
297+
}
298+
299+
private void updatePreference(String tier) {
300+
client().admin().indices().updateSettings(new UpdateSettingsRequest(index)
301+
.settings(org.elasticsearch.common.collect.Map.of(DataTierAllocationDecider.INDEX_ROUTING_PREFER, tier))).actionGet();
302+
}
303+
274304
private DataTiersFeatureSetUsage getUsage() {
275305
XPackUsageResponse usages = new XPackUsageRequestBuilder(client()).execute().actionGet();
276306
XPackFeatureSet.Usage dtUsage = usages.getUsages().stream()

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDecider.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@
2121
import org.elasticsearch.common.settings.ClusterSettings;
2222
import org.elasticsearch.common.settings.Setting;
2323
import org.elasticsearch.common.settings.Settings;
24+
import org.elasticsearch.index.IndexModule;
2425
import org.elasticsearch.xpack.core.DataTier;
26+
import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants;
2527

2628
import java.util.Arrays;
29+
import java.util.Collection;
30+
import java.util.Iterator;
31+
import java.util.Map;
2732
import java.util.Optional;
2833
import java.util.Set;
2934
import java.util.stream.Collectors;
3035

36+
import static org.elasticsearch.xpack.core.DataTier.DATA_FROZEN;
37+
3138
/**
3239
* The {@code DataTierAllocationDecider} is a custom allocation decider that behaves similar to the
3340
* {@link org.elasticsearch.cluster.routing.allocation.decider.FilterAllocationDecider}, however it
@@ -45,20 +52,21 @@ public class DataTierAllocationDecider extends AllocationDecider {
4552
public static final String INDEX_ROUTING_PREFER = "index.routing.allocation.include._tier_preference";
4653
public static final String INDEX_ROUTING_EXCLUDE = "index.routing.allocation.exclude._tier";
4754

55+
private static final DataTierValidator VALIDATOR = new DataTierValidator();
4856
public static final Setting<String> CLUSTER_ROUTING_REQUIRE_SETTING = Setting.simpleString(CLUSTER_ROUTING_REQUIRE,
4957
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
5058
public static final Setting<String> CLUSTER_ROUTING_INCLUDE_SETTING = Setting.simpleString(CLUSTER_ROUTING_INCLUDE,
5159
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
5260
public static final Setting<String> CLUSTER_ROUTING_EXCLUDE_SETTING = Setting.simpleString(CLUSTER_ROUTING_EXCLUDE,
5361
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.NodeScope);
5462
public static final Setting<String> INDEX_ROUTING_REQUIRE_SETTING = Setting.simpleString(INDEX_ROUTING_REQUIRE,
55-
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
63+
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
5664
public static final Setting<String> INDEX_ROUTING_INCLUDE_SETTING = Setting.simpleString(INDEX_ROUTING_INCLUDE,
57-
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
65+
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
5866
public static final Setting<String> INDEX_ROUTING_EXCLUDE_SETTING = Setting.simpleString(INDEX_ROUTING_EXCLUDE,
59-
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
67+
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
6068
public static final Setting<String> INDEX_ROUTING_PREFER_SETTING = Setting.simpleString(INDEX_ROUTING_PREFER,
61-
DataTierAllocationDecider::validateTierSetting, Setting.Property.Dynamic, Setting.Property.IndexScope);
69+
VALIDATOR, Setting.Property.Dynamic, Setting.Property.IndexScope);
6270

6371
private static void validateTierSetting(String setting) {
6472
if (Strings.hasText(setting)) {
@@ -71,6 +79,32 @@ private static void validateTierSetting(String setting) {
7179
}
7280
}
7381

82+
private static class DataTierValidator implements Setting.Validator<String> {
83+
private static final Collection<Setting<?>> dependencies =
84+
org.elasticsearch.common.collect.List.of(IndexModule.INDEX_STORE_TYPE_SETTING,
85+
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING);
86+
87+
@Override
88+
public void validate(String value) {
89+
validateTierSetting(value);
90+
}
91+
92+
@Override
93+
public void validate(String value, Map<Setting<?>, Object> settings) {
94+
if (Strings.hasText(value) && SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(settings) == false) {
95+
String[] split = value.split(",");
96+
if (Arrays.stream(split).anyMatch(DATA_FROZEN::equals)) {
97+
throw new IllegalArgumentException("[" + DATA_FROZEN + "] tier can only be used for partial searchable snapshots");
98+
}
99+
}
100+
}
101+
102+
@Override
103+
public Iterator<Setting<?>> settings() {
104+
return dependencies.iterator();
105+
}
106+
}
107+
74108
private volatile String clusterRequire;
75109
private volatile String clusterInclude;
76110
private volatile String clusterExclude;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsConstants.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,41 @@
77
package org.elasticsearch.xpack.searchablesnapshots;
88

99
import org.elasticsearch.Version;
10+
import org.elasticsearch.common.settings.Setting;
1011
import org.elasticsearch.common.settings.Settings;
1112

13+
import java.util.Map;
14+
1215
import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING;
1316

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

1720
public static final String SNAPSHOT_RECOVERY_STATE_FACTORY_KEY = "snapshot_prewarm";
21+
public static final Setting<Boolean> SNAPSHOT_PARTIAL_SETTING = Setting.boolSetting(
22+
"index.store.snapshot.partial",
23+
false,
24+
Setting.Property.IndexScope,
25+
Setting.Property.PrivateIndex,
26+
Setting.Property.NotCopyableOnResize
27+
);
1828

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

33+
/**
34+
* Based on a map from setting to value, do the settings represent a partial searchable snapshot index?
35+
*
36+
* Both index.store.type and index.store.snapshot.partial must be supplied.
37+
*/
38+
public static boolean isPartialSearchableSnapshotIndex(Map<Setting<?>, Object> indexSettings) {
39+
assert indexSettings.containsKey(INDEX_STORE_TYPE_SETTING) : "must include store type in map";
40+
assert indexSettings.get(SNAPSHOT_PARTIAL_SETTING) != null : "partial setting must be non-null in map (has default value)";
41+
return SNAPSHOT_DIRECTORY_FACTORY_KEY.equals(indexSettings.get(INDEX_STORE_TYPE_SETTING))
42+
&& (boolean) indexSettings.get(SNAPSHOT_PARTIAL_SETTING);
43+
}
44+
2345
public static final String CACHE_FETCH_ASYNC_THREAD_POOL_NAME = "searchable_snapshots_cache_fetch_async";
2446
public static final String CACHE_FETCH_ASYNC_THREAD_POOL_SETTING = "xpack.searchable_snapshots.cache_fetch_async_thread_pool";
2547

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDeciderTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

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

10+
import joptsimple.internal.Strings;
1011
import org.elasticsearch.Version;
1112
import org.elasticsearch.cluster.ClusterState;
1213
import org.elasticsearch.cluster.ESAllocationTestCase;
@@ -27,20 +28,26 @@
2728
import org.elasticsearch.cluster.routing.allocation.decider.Decision;
2829
import org.elasticsearch.cluster.routing.allocation.decider.ReplicaAfterPrimaryActiveAllocationDecider;
2930
import org.elasticsearch.cluster.routing.allocation.decider.SameShardAllocationDecider;
31+
import org.elasticsearch.common.Randomness;
3032
import org.elasticsearch.common.settings.ClusterSettings;
3133
import org.elasticsearch.common.settings.Setting;
3234
import org.elasticsearch.common.settings.Settings;
35+
import org.elasticsearch.index.IndexModule;
3336
import org.elasticsearch.index.shard.ShardId;
3437
import org.elasticsearch.snapshots.EmptySnapshotsInfoService;
3538
import org.elasticsearch.test.gateway.TestGatewayAllocator;
3639
import org.elasticsearch.xpack.core.DataTier;
40+
import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants;
3741

42+
import java.util.ArrayList;
3843
import java.util.Arrays;
3944
import java.util.Collections;
4045
import java.util.HashSet;
46+
import java.util.List;
4147
import java.util.Optional;
4248
import java.util.Set;
4349

50+
import static org.elasticsearch.xpack.core.DataTier.DATA_FROZEN;
4451
import static org.hamcrest.Matchers.containsString;
4552
import static org.hamcrest.Matchers.equalTo;
4653

@@ -702,6 +709,53 @@ public void testExistedClusterFilters() {
702709
"tier filters [data_hot,data_warm]"));
703710
}
704711

712+
public void testFrozenIllegalForRegularIndices() {
713+
List<String> tierList = new ArrayList<>(randomSubsetOf(DataTier.ALL_DATA_TIERS));
714+
if (tierList.contains(DATA_FROZEN) == false) {
715+
tierList.add(DATA_FROZEN);
716+
}
717+
Randomness.shuffle(tierList);
718+
719+
String value = Strings.join(tierList, ",");
720+
Setting<String> setting = randomTierSetting();
721+
Settings.Builder builder = Settings.builder().put(setting.getKey(), value);
722+
if (randomBoolean()) {
723+
builder.put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY);
724+
}
725+
726+
Settings settings = builder.build();
727+
IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> setting.get(settings));
728+
assertThat(exception.getMessage(), equalTo("[data_frozen] tier can only be used for partial searchable snapshots"));
729+
}
730+
731+
public void testFrozenLegalForPartialSnapshot() {
732+
List<String> tierList = new ArrayList<>(randomSubsetOf(DataTier.ALL_DATA_TIERS));
733+
if (tierList.contains(DATA_FROZEN) == false) {
734+
tierList.add(DATA_FROZEN);
735+
}
736+
Randomness.shuffle(tierList);
737+
738+
String value = Strings.join(tierList, ",");
739+
Setting<String> setting = randomTierSetting();
740+
Settings.Builder builder = Settings.builder().put(setting.getKey(), value);
741+
builder.put(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY);
742+
builder.put(SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING.getKey(), true);
743+
744+
Settings settings = builder.build();
745+
746+
// validate do not throw
747+
assertThat(setting.get(settings), equalTo(value));
748+
}
749+
750+
public Setting<String> randomTierSetting() {
751+
//noinspection unchecked
752+
return randomFrom(
753+
DataTierAllocationDecider.INDEX_ROUTING_EXCLUDE_SETTING,
754+
DataTierAllocationDecider.INDEX_ROUTING_INCLUDE_SETTING,
755+
DataTierAllocationDecider.INDEX_ROUTING_REQUIRE_SETTING,
756+
DataTierAllocationDecider.INDEX_ROUTING_PREFER_SETTING);
757+
}
758+
705759
private ClusterState prepareState(ClusterState initialState) {
706760
return prepareState(initialState, Settings.EMPTY);
707761
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.searchablesnapshots;
9+
10+
import org.elasticsearch.index.IndexModule;
11+
import org.elasticsearch.test.ESTestCase;
12+
13+
import org.elasticsearch.common.collect.Map;
14+
15+
import static org.hamcrest.Matchers.is;
16+
17+
public class SearchableSnapshotsConstantsTests extends ESTestCase {
18+
19+
public void testIsPartialSearchableSnapshotIndex() {
20+
assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
21+
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY,
22+
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, false)),
23+
is(false));
24+
25+
assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
26+
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, "abc",
27+
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, randomBoolean())),
28+
is(false));
29+
30+
assertThat(SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(
31+
Map.of(IndexModule.INDEX_STORE_TYPE_SETTING, SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY,
32+
SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING, true)),
33+
is(true));
34+
}
35+
}

x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ protected void mountSnapshot(
157157
storage
158158
);
159159

160-
final RestoreSnapshotResponse restoreResponse = client().execute(MountSearchableSnapshotAction.INSTANCE, mountRequest).get();
160+
final RestoreSnapshotResponse restoreResponse = client().execute(MountSearchableSnapshotAction.INSTANCE, mountRequest).actionGet();
161161
assertThat(restoreResponse.getRestoreInfo().successfulShards(), equalTo(getNumShards(restoredIndexName).numPrimaries));
162162
assertThat(restoreResponse.getRestoreInfo().failedShards(), equalTo(0));
163163
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.searchablesnapshots;
9+
10+
import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
11+
import org.elasticsearch.common.settings.Settings;
12+
import org.elasticsearch.test.ESIntegTestCase;
13+
import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider;
14+
import org.elasticsearch.xpack.core.DataTier;
15+
import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;
16+
17+
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST)
18+
public class SearchableSnapshotDataTierIntegTests extends BaseSearchableSnapshotsIntegTestCase {
19+
20+
private static final String repoName = "test-repo";
21+
private static final String indexName = "test-index";
22+
private static final String snapshotName = "test-snapshot";
23+
private static final String mountedIndexName = "test-index-mounted";
24+
private static final Settings frozenSettings = Settings.builder()
25+
.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_FROZEN)
26+
.build();
27+
28+
public void testPartialLegalOnFrozen() throws Exception {
29+
createRepository(repoName, "fs");
30+
createIndex(indexName);
31+
createFullSnapshot(repoName, snapshotName);
32+
Settings mountSettings = randomFrom(Settings.EMPTY, frozenSettings);
33+
mountSnapshot(
34+
repoName,
35+
snapshotName,
36+
indexName,
37+
mountedIndexName,
38+
mountSettings,
39+
MountSearchableSnapshotRequest.Storage.SHARED_CACHE
40+
);
41+
42+
updatePreference(DataTier.DATA_FROZEN);
43+
}
44+
45+
public void testFullIllegalOnFrozen() throws Exception {
46+
createRepository(repoName, "fs");
47+
createIndex(indexName);
48+
createFullSnapshot(repoName, snapshotName);
49+
expectThrows(
50+
IllegalArgumentException.class,
51+
() -> mountSnapshot(
52+
repoName,
53+
snapshotName,
54+
indexName,
55+
mountedIndexName,
56+
frozenSettings,
57+
MountSearchableSnapshotRequest.Storage.FULL_COPY
58+
)
59+
);
60+
Settings mountSettings = randomFrom(
61+
Settings.EMPTY,
62+
Settings.builder()
63+
.put(
64+
DataTierAllocationDecider.INDEX_ROUTING_PREFER,
65+
randomValueOtherThan(DataTier.DATA_FROZEN, () -> randomFrom(DataTier.ALL_DATA_TIERS))
66+
)
67+
.build()
68+
);
69+
mountSnapshot(repoName, snapshotName, indexName, mountedIndexName, mountSettings, MountSearchableSnapshotRequest.Storage.FULL_COPY);
70+
71+
expectThrows(IllegalArgumentException.class, () -> updatePreference(DataTier.DATA_FROZEN));
72+
}
73+
74+
private void updatePreference(String tier) {
75+
client().admin()
76+
.indices()
77+
.updateSettings(
78+
new UpdateSettingsRequest(mountedIndexName).settings(
79+
org.elasticsearch.common.collect.Map.of(DataTierAllocationDecider.INDEX_ROUTING_PREFER, tier)
80+
)
81+
)
82+
.actionGet();
83+
}
84+
}

0 commit comments

Comments
 (0)