Skip to content

Commit 046cdea

Browse files
authored
Introduce lazy rollover for mapping updates in data streams (#103309)
In this PR we implement the idea to introduce a flag, that a data stream needs to be rolloved over before the next document is indexed.
1 parent af916a0 commit 046cdea

File tree

35 files changed

+868
-168
lines changed

35 files changed

+868
-168
lines changed

docs/changelog/103309.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 103309
2+
summary: Introduce lazy rollover for mapping updates in data streams
3+
area: Data streams
4+
type: enhancement
5+
issues:
6+
- 89346

docs/reference/data-streams/change-mappings-and-settings.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@ stream's oldest backing index.
592592
"hidden": false,
593593
"system": false,
594594
"allow_custom_routing": false,
595-
"replicated": false
595+
"replicated": false,
596+
"rollover_on_write": false
596597
}
597598
]
598599
}

docs/reference/data-streams/downsampling-ilm.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ following. Note the original `index_name`: `.ds-datastream-<timestamp>-000001`.
326326
"system": false,
327327
"allow_custom_routing": false,
328328
"replicated": false,
329+
"rollover_on_write": false,
329330
"time_series": {
330331
"temporal_ranges": [
331332
{

docs/reference/data-streams/downsampling-manual.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ This returns:
372372
"system": false,
373373
"allow_custom_routing": false,
374374
"replicated": false,
375+
"rollover_on_write": false,
375376
"time_series": {
376377
"temporal_ranges": [
377378
{

docs/reference/data-streams/lifecycle/tutorial-migrate-data-stream-from-ilm-to-dsl.asciidoc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ and that the next generation index will also be managed by {ilm-init}:
139139
"hidden": false,
140140
"system": false,
141141
"allow_custom_routing": false,
142-
"replicated": false
142+
"replicated": false,
143+
"rollover_on_write": false
143144
}
144145
]
145146
}
@@ -275,7 +276,8 @@ GET _data_stream/dsl-data-stream
275276
"hidden": false,
276277
"system": false,
277278
"allow_custom_routing": false,
278-
"replicated": false
279+
"replicated": false,
280+
"rollover_on_write": false
279281
}
280282
]
281283
}
@@ -352,7 +354,8 @@ GET _data_stream/dsl-data-stream
352354
"hidden": false,
353355
"system": false,
354356
"allow_custom_routing": false,
355-
"replicated": false
357+
"replicated": false,
358+
"rollover_on_write": false
356359
}
357360
]
358361
}
@@ -449,7 +452,8 @@ GET _data_stream/dsl-data-stream
449452
"hidden": false,
450453
"system": false,
451454
"allow_custom_routing": false,
452-
"replicated": false
455+
"replicated": false,
456+
"rollover_on_write": false
453457
}
454458
]
455459
}

docs/reference/data-streams/use-a-data-stream.asciidoc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,24 @@ GET /_data_stream/my-data-stream/_stats?human=true
117117
=== Manually roll over a data stream
118118

119119
Use the <<indices-rollover-index,rollover API>> to manually
120-
<<data-streams-rollover,roll over>> a data stream:
120+
<<data-streams-rollover,roll over>> a data stream. You have
121+
two options when manually rolling over:
121122

123+
1. To immediately trigger a rollover:
124+
+
122125
[source,console]
123126
----
124127
POST /my-data-stream/_rollover/
125128
----
129+
2. Or to postpone the rollover until the next indexing event occurs:
130+
+
131+
[source,console]
132+
----
133+
POST /my-data-stream/_rollover?lazy
134+
----
135+
+
136+
Use the second to avoid having empty backing indices in data streams
137+
that do not get updated often.
126138

127139
[discrete]
128140
[[open-closed-backing-indices]]

docs/reference/indices/get-data-stream.asciidoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ The conditions which will trigger the rollover of a backing index as configured
262262
`cluster.lifecycle.default.rollover`. This property is an implementation detail and it will only be retrieved when the query
263263
param `include_defaults` is set to `true`. The contents of this field are subject to change.
264264
=====
265+
266+
`rollover_on_write`::
267+
(Boolean)
268+
If `true`, the next write to this data stream will trigger a rollover first and the document will be
269+
indexed in the new backing index. If the rollover fails the indexing request will fail too.
265270
====
266271

267272
[[get-data-stream-api-example]]
@@ -311,7 +316,8 @@ The API returns the following response:
311316
"hidden": false,
312317
"system": false,
313318
"allow_custom_routing": false,
314-
"replicated": false
319+
"replicated": false,
320+
"rollover_on_write": false
315321
},
316322
{
317323
"name": "my-data-stream-two",
@@ -339,7 +345,8 @@ The API returns the following response:
339345
"hidden": false,
340346
"system": false,
341347
"allow_custom_routing": false,
342-
"replicated": false
348+
"replicated": false,
349+
"rollover_on_write": false
343350
}
344351
]
345352
}

docs/reference/indices/rollover-index.asciidoc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ include::{es-repo-dir}/indices/create-index.asciidoc[tag=index-name-reqs]
114114
If `true`, checks whether the current index satisfies the specified
115115
`conditions` but does not perform a rollover. Defaults to `false`.
116116

117+
`lazy`::
118+
(Optional, Boolean)
119+
If `true`, signals that the data stream will be rolled over when the next
120+
indexing operation occurs. Applies only to data streams. Defaults to `false`.
121+
117122
include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=wait_for_active_shards]
118123

119124
include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=timeoutparms]
@@ -204,6 +209,11 @@ conditions were specified, this is an empty object.
204209
index met the condition.
205210
====
206211

212+
`lazy`::
213+
(Boolean)
214+
If `true`, {es} did not perform the rollover, but successfully marked the data stream to be rolled
215+
over at the next indexing event.
216+
207217
[[rollover-index-api-example]]
208218
==== {api-examples-title}
209219

@@ -218,6 +228,17 @@ POST my-data-stream/_rollover
218228
----
219229
// TEST[setup:my_data_stream]
220230

231+
The following request rolls over a data stream lazily, meaning that the data stream
232+
will roll over at the next indexing event. This ensures that mapping and setting changes
233+
will be applied to the coming data, but it will avoid creating extra backing indices for
234+
data streams with slow ingestion.
235+
236+
[source,console]
237+
----
238+
POST my-data-stream/_rollover?lazy
239+
----
240+
// TEST[continued]
241+
221242
:target: data stream
222243
:index: write index
223244

@@ -257,6 +278,7 @@ The API returns:
257278
"new_index": ".ds-my-data-stream-2099.05.07-000002",
258279
"rolled_over": true,
259280
"dry_run": false,
281+
"lazy": false,
260282
"conditions": {
261283
"[max_age: 7d]": false,
262284
"[max_docs: 1000]": true,
@@ -328,6 +350,7 @@ The API returns:
328350
"new_index": "my-index-2099.05.07-000002",
329351
"rolled_over": true,
330352
"dry_run": false,
353+
"lazy": false,
331354
"conditions": {
332355
"[max_age: 7d]": false,
333356
"[max_docs: 1000]": true,
@@ -399,6 +422,7 @@ The API returns:
399422
"new_index": "my-index-2099.05.07-000002",
400423
"rolled_over": true,
401424
"dry_run": false,
425+
"lazy": false,
402426
"conditions": {
403427
"[max_age: 7d]": false,
404428
"[max_docs: 1000]": true,
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.datastreams;
10+
11+
import org.elasticsearch.client.Request;
12+
import org.elasticsearch.client.Response;
13+
import org.elasticsearch.client.ResponseException;
14+
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
import static org.hamcrest.Matchers.containsString;
19+
import static org.hamcrest.Matchers.endsWith;
20+
import static org.hamcrest.Matchers.equalTo;
21+
import static org.hamcrest.Matchers.is;
22+
import static org.hamcrest.Matchers.startsWith;
23+
24+
public class LazyRolloverDataStreamIT extends DisabledSecurityDataStreamTestCase {
25+
26+
@SuppressWarnings("unchecked")
27+
public void testLazyRollover() throws Exception {
28+
Request putComposableIndexTemplateRequest = new Request("POST", "/_index_template/lazy-ds-template");
29+
putComposableIndexTemplateRequest.setJsonEntity("""
30+
{
31+
"index_patterns": ["lazy-ds*"],
32+
"data_stream": {}
33+
}
34+
""");
35+
assertOK(client().performRequest(putComposableIndexTemplateRequest));
36+
37+
String dataStreamName = "lazy-ds";
38+
39+
Request createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
40+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-22\", \"a\": 1 }");
41+
assertOK(client().performRequest(createDocRequest));
42+
43+
final Response rolloverResponse = client().performRequest(new Request("POST", "/" + dataStreamName + "/_rollover?lazy"));
44+
Map<String, Object> rolloverResponseMap = entityAsMap(rolloverResponse);
45+
assertThat((String) rolloverResponseMap.get("old_index"), startsWith(".ds-lazy-ds-"));
46+
assertThat((String) rolloverResponseMap.get("old_index"), endsWith("-000001"));
47+
assertThat((String) rolloverResponseMap.get("new_index"), startsWith(".ds-lazy-ds-"));
48+
assertThat((String) rolloverResponseMap.get("new_index"), endsWith("-000002"));
49+
assertThat(rolloverResponseMap.get("lazy"), equalTo(true));
50+
assertThat(rolloverResponseMap.get("dry_run"), equalTo(false));
51+
assertThat(rolloverResponseMap.get("acknowledged"), equalTo(true));
52+
assertThat(rolloverResponseMap.get("rolled_over"), equalTo(false));
53+
assertThat(rolloverResponseMap.get("conditions"), equalTo(Map.of()));
54+
55+
{
56+
final Response dataStreamResponse = client().performRequest(new Request("GET", "/_data_stream/" + dataStreamName));
57+
List<Object> dataStreams = (List<Object>) entityAsMap(dataStreamResponse).get("data_streams");
58+
assertThat(dataStreams.size(), is(1));
59+
Map<String, Object> dataStream = (Map<String, Object>) dataStreams.get(0);
60+
assertThat(dataStream.get("name"), equalTo(dataStreamName));
61+
assertThat(dataStream.get("rollover_on_write"), is(true));
62+
assertThat(((List<Object>) dataStream.get("indices")).size(), is(1));
63+
}
64+
65+
createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
66+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-23\", \"a\": 2 }");
67+
assertOK(client().performRequest(createDocRequest));
68+
69+
{
70+
final Response dataStreamResponse = client().performRequest(new Request("GET", "/_data_stream/" + dataStreamName));
71+
List<Object> dataStreams = (List<Object>) entityAsMap(dataStreamResponse).get("data_streams");
72+
assertThat(dataStreams.size(), is(1));
73+
Map<String, Object> dataStream = (Map<String, Object>) dataStreams.get(0);
74+
assertThat(dataStream.get("name"), equalTo(dataStreamName));
75+
assertThat(dataStream.get("rollover_on_write"), is(false));
76+
assertThat(((List<Object>) dataStream.get("indices")).size(), is(2));
77+
}
78+
}
79+
80+
@SuppressWarnings("unchecked")
81+
public void testLazyRolloverFailsIndexing() throws Exception {
82+
Request putComposableIndexTemplateRequest = new Request("POST", "/_index_template/lazy-ds-template");
83+
putComposableIndexTemplateRequest.setJsonEntity("""
84+
{
85+
"index_patterns": ["lazy-ds*"],
86+
"data_stream": {}
87+
}
88+
""");
89+
assertOK(client().performRequest(putComposableIndexTemplateRequest));
90+
91+
String dataStreamName = "lazy-ds";
92+
93+
Request createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
94+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-22\", \"a\": 1 }");
95+
assertOK(client().performRequest(createDocRequest));
96+
97+
Request updateClusterSettingsRequest = new Request("PUT", "_cluster/settings");
98+
updateClusterSettingsRequest.setJsonEntity("""
99+
{
100+
"persistent": {
101+
"cluster.max_shards_per_node": 1
102+
}
103+
}""");
104+
assertAcknowledged(client().performRequest(updateClusterSettingsRequest));
105+
106+
final Response rolloverResponse = client().performRequest(new Request("POST", "/" + dataStreamName + "/_rollover?lazy"));
107+
Map<String, Object> rolloverResponseMap = entityAsMap(rolloverResponse);
108+
assertThat((String) rolloverResponseMap.get("old_index"), startsWith(".ds-lazy-ds-"));
109+
assertThat((String) rolloverResponseMap.get("old_index"), endsWith("-000001"));
110+
assertThat((String) rolloverResponseMap.get("new_index"), startsWith(".ds-lazy-ds-"));
111+
assertThat((String) rolloverResponseMap.get("new_index"), endsWith("-000002"));
112+
assertThat(rolloverResponseMap.get("lazy"), equalTo(true));
113+
assertThat(rolloverResponseMap.get("dry_run"), equalTo(false));
114+
assertThat(rolloverResponseMap.get("acknowledged"), equalTo(true));
115+
assertThat(rolloverResponseMap.get("rolled_over"), equalTo(false));
116+
assertThat(rolloverResponseMap.get("conditions"), equalTo(Map.of()));
117+
118+
{
119+
final Response dataStreamResponse = client().performRequest(new Request("GET", "/_data_stream/" + dataStreamName));
120+
List<Object> dataStreams = (List<Object>) entityAsMap(dataStreamResponse).get("data_streams");
121+
assertThat(dataStreams.size(), is(1));
122+
Map<String, Object> dataStream = (Map<String, Object>) dataStreams.get(0);
123+
assertThat(dataStream.get("name"), equalTo(dataStreamName));
124+
assertThat(dataStream.get("rollover_on_write"), is(true));
125+
assertThat(((List<Object>) dataStream.get("indices")).size(), is(1));
126+
}
127+
128+
try {
129+
createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
130+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-23\", \"a\": 2 }");
131+
client().performRequest(createDocRequest);
132+
fail("Indexing should have failed.");
133+
} catch (ResponseException responseException) {
134+
assertThat(responseException.getMessage(), containsString("this action would add [2] shards"));
135+
}
136+
137+
updateClusterSettingsRequest = new Request("PUT", "_cluster/settings");
138+
updateClusterSettingsRequest.setJsonEntity("""
139+
{
140+
"persistent": {
141+
"cluster.max_shards_per_node": null
142+
}
143+
}""");
144+
assertAcknowledged(client().performRequest(updateClusterSettingsRequest));
145+
createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
146+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-23\", \"a\": 2 }");
147+
assertOK(client().performRequest(createDocRequest));
148+
{
149+
final Response dataStreamResponse = client().performRequest(new Request("GET", "/_data_stream/" + dataStreamName));
150+
List<Object> dataStreams = (List<Object>) entityAsMap(dataStreamResponse).get("data_streams");
151+
assertThat(dataStreams.size(), is(1));
152+
Map<String, Object> dataStream = (Map<String, Object>) dataStreams.get(0);
153+
assertThat(dataStream.get("name"), equalTo(dataStreamName));
154+
assertThat(dataStream.get("rollover_on_write"), is(false));
155+
assertThat(((List<Object>) dataStream.get("indices")).size(), is(2));
156+
}
157+
}
158+
159+
@SuppressWarnings("unchecked")
160+
public void testLazyRolloverWithConditions() throws Exception {
161+
Request putComposableIndexTemplateRequest = new Request("POST", "/_index_template/lazy-ds-template");
162+
putComposableIndexTemplateRequest.setJsonEntity("""
163+
{
164+
"index_patterns": ["lazy-ds*"],
165+
"data_stream": {}
166+
}
167+
""");
168+
assertOK(client().performRequest(putComposableIndexTemplateRequest));
169+
170+
String dataStreamName = "lazy-ds";
171+
172+
Request createDocRequest = new Request("POST", "/" + dataStreamName + "/_doc?refresh=true");
173+
createDocRequest.setJsonEntity("{ \"@timestamp\": \"2020-10-22\", \"a\": 1 }");
174+
175+
assertOK(client().performRequest(createDocRequest));
176+
177+
Request rolloverRequest = new Request("POST", "/" + dataStreamName + "/_rollover?lazy");
178+
rolloverRequest.setJsonEntity("{\"conditions\": {\"max_docs\": 1}}");
179+
ResponseException responseError = expectThrows(ResponseException.class, () -> client().performRequest(rolloverRequest));
180+
assertThat(responseError.getResponse().getStatusLine().getStatusCode(), is(400));
181+
assertThat(responseError.getMessage(), containsString("only without any conditions"));
182+
}
183+
}

qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/BlockedSearcherRestCancellationTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
*/
5656
public abstract class BlockedSearcherRestCancellationTestCase extends HttpSmokeTestCase {
5757

58-
private static final Setting<Boolean> BLOCK_SEARCHER_SETTING = Setting.boolSetting(
58+
protected static final Setting<Boolean> BLOCK_SEARCHER_SETTING = Setting.boolSetting(
5959
"index.block_searcher",
6060
false,
6161
Setting.Property.IndexScope

0 commit comments

Comments
 (0)