-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Introduce "Feature States" for managing snapshots of system indices #63513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 147 commits
65903ab
6d32a65
37e4f4b
1fefb26
1869e91
ac2961f
febcdd7
6561940
c16e472
0c53a22
f0ef426
1f4ecfe
f824abe
040fda6
013b764
9120ffa
26e94f6
df3deff
ce24b74
9382885
261f5c0
89d8e19
c207993
08406bd
d2b517b
93af221
b6392af
e4a4580
b7ef98e
9197bb8
8d6cbcb
27df301
a344a65
2a5c42a
2ff22b1
7ecabdc
f069cf1
93c96df
9a1ee0f
06898ba
b86dcf6
4b5cb08
1966575
e3efb29
c8ed77a
e9b25ac
a8398fa
87fde96
8ccbba6
410b8bb
c262c0b
952ffb0
aa32eb5
7ba77bd
d544f1e
d2adae1
88c6c41
c3f8813
64f60d0
ab4106a
031e86e
9b6d1ff
f24ac78
c8ca4a1
c16d60f
3b0fb7f
cfbfbcb
d0b3a44
091ca4c
05fd6d7
4af0a24
439679d
376e264
3c3b1b8
d36f6c8
ef88902
10f2802
e9b0f80
2e54c94
7300c0c
2b9945b
cdc3bd1
9520873
a0baac2
262760f
b416193
2dde730
6831671
510ab18
20c6b98
15b4903
792d735
b375b4b
85b90e8
3909cfc
86ed638
0a70ae1
9f16920
2e652fa
178e941
afdf54c
8c324fd
eec5785
1b99941
055adf2
290b203
f7a0dc2
774f11c
82cfdac
46f5eed
8be4e74
1bec52b
59ab240
b938323
201eec6
7ceecae
6c4669f
596c9ac
d677aeb
8946c94
1476d8d
884983c
1dc1a80
1e37dc6
8f038ee
898a3b3
fe203f6
dc2ac03
1e65d12
fda3a9c
455f762
ba8aba3
4c5ce56
eeda4e1
b78fbcd
facf8fa
4bc6bae
56fbb20
6b1cf06
12d1b25
699c2ae
d503868
f2d7bbe
ef0dda4
6d9edd8
2b52037
38022dd
c5d68ea
873de63
6dd31f1
66bc5cd
90bf18d
dd8889f
d300586
b636420
6930e00
d4267d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.client.snapshots; | ||
|
||
import org.elasticsearch.client.TimedRequest; | ||
|
||
/** | ||
* A {@link TimedRequest} to get the list of features available to be included in snapshots in the cluster. | ||
*/ | ||
public class GetSnapshottableFeaturesRequest extends TimedRequest { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.client.snapshots; | ||
|
||
import org.elasticsearch.common.ParseField; | ||
import org.elasticsearch.common.xcontent.ConstructingObjectParser; | ||
import org.elasticsearch.common.xcontent.ObjectParser; | ||
import org.elasticsearch.common.xcontent.XContentParser; | ||
|
||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
public class GetSnapshottableFeaturesResponse { | ||
|
||
private final List<SnapshottableFeature> features; | ||
|
||
private static final ParseField FEATURES = new ParseField("features"); | ||
|
||
@SuppressWarnings("unchecked") | ||
private static final ConstructingObjectParser<GetSnapshottableFeaturesResponse, Void> PARSER = new ConstructingObjectParser<>( | ||
"snapshottable_features_response", true, (a, ctx) -> new GetSnapshottableFeaturesResponse((List<SnapshottableFeature>) a[0]) | ||
); | ||
|
||
static { | ||
PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), SnapshottableFeature::parse, FEATURES); | ||
} | ||
|
||
public GetSnapshottableFeaturesResponse(List<SnapshottableFeature> features) { | ||
this.features = features; | ||
} | ||
|
||
public List<SnapshottableFeature> getFeatures() { | ||
return features; | ||
} | ||
|
||
public static GetSnapshottableFeaturesResponse parse(XContentParser parser) { | ||
return PARSER.apply(parser, null); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (!(o instanceof GetSnapshottableFeaturesResponse)) return false; | ||
GetSnapshottableFeaturesResponse that = (GetSnapshottableFeaturesResponse) o; | ||
return getFeatures().equals(that.getFeatures()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(getFeatures()); | ||
} | ||
|
||
public static class SnapshottableFeature { | ||
|
||
private final String featureName; | ||
private final String description; | ||
|
||
private static final ParseField FEATURE_NAME = new ParseField("name"); | ||
private static final ParseField DESCRIPTION = new ParseField("description"); | ||
|
||
private static final ConstructingObjectParser<SnapshottableFeature, Void> PARSER = new ConstructingObjectParser<>( | ||
"feature", true, (a, ctx) -> new SnapshottableFeature((String) a[0], (String) a[1]) | ||
); | ||
|
||
static { | ||
PARSER.declareField(ConstructingObjectParser.constructorArg(), | ||
(p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING); | ||
PARSER.declareField(ConstructingObjectParser.constructorArg(), | ||
(p, c) -> p.text(), DESCRIPTION, ObjectParser.ValueType.STRING); | ||
} | ||
|
||
public SnapshottableFeature(String featureName, String description) { | ||
this.featureName = featureName; | ||
this.description = description; | ||
} | ||
|
||
public static SnapshottableFeature parse(XContentParser parser, Void ctx) { | ||
return PARSER.apply(parser, ctx); | ||
} | ||
|
||
public String getFeatureName() { | ||
return featureName; | ||
} | ||
|
||
public String getDescription() { | ||
return description; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (!(o instanceof SnapshottableFeature)) return false; | ||
SnapshottableFeature feature = (SnapshottableFeature) o; | ||
return Objects.equals(getFeatureName(), feature.getFeatureName()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(getFeatureName()); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ | |
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; | ||
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; | ||
import org.elasticsearch.action.support.master.AcknowledgedResponse; | ||
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest; | ||
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesResponse; | ||
import org.elasticsearch.cluster.metadata.IndexMetadata; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.xcontent.XContentType; | ||
|
@@ -39,12 +41,16 @@ | |
import java.io.IOException; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EDIT: please revert noisy format changes in this PR, they're all over the place now that I read through it in full :) |
||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import static org.elasticsearch.snapshots.SnapshotsService.NO_FEATURE_STATES_VALUE; | ||
import static org.elasticsearch.tasks.TaskResultsService.TASKS_FEATURE_NAME; | ||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.greaterThan; | ||
import static org.hamcrest.Matchers.hasSize; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.hamcrest.Matchers.notNullValue; | ||
|
||
public class SnapshotIT extends ESRestHighLevelClientTestCase { | ||
|
||
|
@@ -150,6 +156,14 @@ public void testCreateSnapshot() throws Exception { | |
} | ||
request.partial(randomBoolean()); | ||
request.includeGlobalState(randomBoolean()); | ||
final List<String> featureStates = randomFrom( | ||
List.of( | ||
Collections.emptyList(), | ||
Collections.singletonList(TASKS_FEATURE_NAME), | ||
Collections.singletonList(NO_FEATURE_STATES_VALUE) | ||
) | ||
); | ||
request.featureStates(featureStates); | ||
|
||
CreateSnapshotResponse response = createTestSnapshot(request); | ||
assertEquals(waitForCompletion ? RestStatus.OK : RestStatus.ACCEPTED, response.status()); | ||
|
@@ -262,9 +276,14 @@ public void testRestoreSnapshot() throws IOException { | |
assertFalse("index [" + testIndex + "] should have been deleted", indexExists(testIndex)); | ||
|
||
RestoreSnapshotRequest request = new RestoreSnapshotRequest(testRepository, testSnapshot); | ||
request.indices(testIndex); | ||
request.waitForCompletion(true); | ||
request.renamePattern(testIndex); | ||
request.renameReplacement(restoredIndex); | ||
if (randomBoolean()) { | ||
request.includeGlobalState(true); | ||
request.featureStates(Collections.singletonList(NO_FEATURE_STATES_VALUE)); | ||
} | ||
|
||
RestoreSnapshotResponse response = execute(request, highLevelClient().snapshot()::restore, | ||
highLevelClient().snapshot()::restoreAsync); | ||
|
@@ -364,6 +383,18 @@ public void testCloneSnapshot() throws IOException { | |
assertTrue(response.isAcknowledged()); | ||
} | ||
|
||
public void testGetFeatures() throws IOException { | ||
GetSnapshottableFeaturesRequest request = new GetSnapshottableFeaturesRequest(); | ||
|
||
GetSnapshottableFeaturesResponse response = execute(request, | ||
highLevelClient().snapshot()::getFeatures, highLevelClient().snapshot()::getFeaturesAsync); | ||
|
||
assertThat(response, notNullValue()); | ||
assertThat(response.getFeatures(), notNullValue()); | ||
assertThat(response.getFeatures().size(), greaterThan(1)); | ||
assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName()))); | ||
} | ||
|
||
private static Map<String, Object> randomUserMetadata() { | ||
if (randomBoolean()) { | ||
return null; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.client.snapshots; | ||
|
||
import org.elasticsearch.client.AbstractResponseTestCase; | ||
import org.elasticsearch.common.xcontent.XContentParser; | ||
import org.elasticsearch.common.xcontent.XContentType; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.hamcrest.Matchers.everyItem; | ||
import static org.hamcrest.Matchers.hasSize; | ||
import static org.hamcrest.Matchers.in; | ||
import static org.hamcrest.Matchers.is; | ||
|
||
public class GetSnapshottableFeaturesResponseTests extends AbstractResponseTestCase< | ||
org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse, | ||
GetSnapshottableFeaturesResponse> { | ||
|
||
@Override | ||
protected org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse createServerTestInstance( | ||
XContentType xContentType | ||
) { | ||
return new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse( | ||
randomList( | ||
10, | ||
() -> new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse.SnapshottableFeature( | ||
randomAlphaOfLengthBetween(4, 10), | ||
randomAlphaOfLengthBetween(5, 10) | ||
) | ||
) | ||
); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: random empty line |
||
} | ||
|
||
@Override | ||
protected GetSnapshottableFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException { | ||
return GetSnapshottableFeaturesResponse.parse(parser); | ||
} | ||
|
||
@Override | ||
protected void assertInstances( | ||
org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse serverTestInstance, | ||
GetSnapshottableFeaturesResponse clientInstance | ||
) { | ||
assertNotNull(serverTestInstance.getSnapshottableFeatures()); | ||
assertNotNull(serverTestInstance.getSnapshottableFeatures()); | ||
|
||
assertThat(clientInstance.getFeatures(), hasSize(serverTestInstance.getSnapshottableFeatures().size())); | ||
|
||
Map<String, String> clientFeatures = clientInstance.getFeatures() | ||
.stream() | ||
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription())); | ||
Map<String, String> serverFeatures = serverTestInstance.getSnapshottableFeatures() | ||
.stream() | ||
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription())); | ||
|
||
assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet())))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,20 +99,35 @@ argument is provided, the snapshot only includes the specified data streams and | |
+ | ||
-- | ||
(Optional, Boolean) | ||
If `true`, the current cluster state is included in the snapshot. | ||
If `true`, the current global state is included in the snapshot. | ||
Defaults to `true`. | ||
|
||
The cluster state includes: | ||
The global state includes: | ||
|
||
* Persistent cluster settings | ||
* Index templates | ||
* Legacy index templates | ||
* Ingest pipelines | ||
* {ilm-init} lifecycle policies | ||
* Data stored in system indices, such as Watches and task records (configurable via `feature_states`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There isn't a great place to leave this comment but this is in the section about what the |
||
-- | ||
+ | ||
IMPORTANT: By default, the entire snapshot will fail if one or more indices included in the snapshot do not have all primary shards available. You can change this behavior by setting <<create-snapshot-api-partial,`partial`>> to `true`. | ||
|
||
[[create-snapshot-api-feature-states]] | ||
`feature_states`:: | ||
(Optional, array of strings) | ||
A list of feature states to be included in this snapshot. A list of features | ||
available for inclusion in the snapshot and their descriptions be can be | ||
retrieved using the <<get-snapshottable-features-api,get snapshottable features API>>. | ||
Each feature state includes one or more system indices containing data necessary | ||
for the function of that feature. Providing an empty array will include no feature | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have concerns regarding the consistency of what an empty array means. For indices within a CreateSnapshotRequest, it means all open indices. The behavior for features states is different and this feels wrong to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fair and a good point - my intent with making it that way was to be able to say "I want to snapshot/restore my cluster state, and no system indices". Do you have any suggestions for an alternate way of doing that? Alternatively, is that even something we need at all? This feels like something that should be possible given you can omit features, but I don't necessarily have a use case in mind. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few items that I can think of would be introducing a special Regarding whether we need it, I guess there could be a case where someone just wants to restore how they did previously and only include items from the cluster state. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My favorite of those is the When feature states are completely omitted I think we should continue to base the default on Introducing +/- support would also be workable, but I think that makes usage more complex. I don't like pattern support here, since feature names don't fall into patterns like indices often do - I think that would just make it more confusing (e.g. "Do I have to say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that the |
||
states in the snapshot, regardless of the value of `include_global_state`. | ||
+ | ||
By default, all available feature states will be included in the snapshot if | ||
`include_global_state` is `true`, or no feature states if `include_global_state` | ||
is `false`. | ||
|
||
include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=master-timeout] | ||
|
||
`metadata`:: | ||
|
@@ -163,6 +178,7 @@ The API returns the following response: | |
"version": <version>, | ||
"indices": [], | ||
"data_streams": [], | ||
"feature_states": [], | ||
"include_global_state": false, | ||
"metadata": { | ||
"taken_by": "user123", | ||
|
Uh oh!
There was an error while loading. Please reload this page.