Skip to content

Commit df9b97a

Browse files
tlrxjkakavas
authored andcommitted
Force Merge should reject requests with only_expunge_deletes and max_num_segments set (#44761)
This commit changes the ForceMergeRequest.validate() method so that it does not accept the parameters only_expunge_deletes and max_num_segments to be set at the same time. The motivation is that InternalEngine.forceMerge() just ignores the max. number of segments parameter when the only expunge parameter is set to true, leaving the wrong impression to the user that max. number of segments has been applied. It also changes InternalEngine.forceMerge() so that it now throws an exception when both parameters are set, and modifies tests where needed. Because it changes the behavior of the REST API I marked this as >breaking. Closes #43102
1 parent 944c998 commit df9b97a

File tree

8 files changed

+124
-8
lines changed

8 files changed

+124
-8
lines changed

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java

+4
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,10 @@ public void testForceMergeIndex() throws Exception {
13131313
request.onlyExpungeDeletes(true); // <1>
13141314
// end::force-merge-request-only-expunge-deletes
13151315

1316+
// set only expunge deletes back to its default value
1317+
// as it is mutually exclusive with max. num. segments
1318+
request.onlyExpungeDeletes(ForceMergeRequest.Defaults.ONLY_EXPUNGE_DELETES);
1319+
13161320
// tag::force-merge-request-flush
13171321
request.flush(true); // <1>
13181322
// end::force-merge-request-flush

docs/reference/migration/migrate_8_0.asciidoc

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ coming[8.0.0]
2727
* <<breaking_80_reindex_changes>>
2828
* <<breaking_80_search_changes>>
2929
* <<breaking_80_settings_changes>>
30+
* <<breaking_80_indices_changes>>
3031

3132
//NOTE: The notable-breaking-changes tagged regions are re-used in the
3233
//Installation and Upgrade Guide
@@ -65,3 +66,4 @@ include::migrate_8_0/http.asciidoc[]
6566
include::migrate_8_0/reindex.asciidoc[]
6667
include::migrate_8_0/search.asciidoc[]
6768
include::migrate_8_0/settings.asciidoc[]
69+
include::migrate_8_0/indices.asciidoc[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[float]
2+
[[breaking_80_indices_changes]]
3+
=== Force Merge API changes
4+
5+
Previously, the Force Merge API allowed the parameters `only_expunge_deletes`
6+
and `max_num_segments` to be set to a non default value at the same time. But
7+
the `max_num_segments` was silently ignored when `only_expunge_deletes` is set
8+
to `true`, leaving the false impression that it has been applied.
9+
10+
The Force Merge API now rejects requests that have a `max_num_segments` greater
11+
than or equal to 0 when the `only_expunge_deletes` is set to true.

rest-api-spec/src/main/resources/rest-api-spec/test/indices.forcemerge/10_basic.yml

+21
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,24 @@
88
indices.forcemerge:
99
index: testing
1010
max_num_segments: 1
11+
12+
---
13+
"Force merge with incompatible only_expunge_deletes and max_num_segments values":
14+
- skip:
15+
version: " - 7.9.99"
16+
reason: only_expunge_deletes and max_num_segments are mutually exclusive since 8.0
17+
18+
- do:
19+
indices.create:
20+
index: test
21+
22+
- do:
23+
catch: bad_request
24+
indices.forcemerge:
25+
index: test
26+
max_num_segments: 10
27+
only_expunge_deletes: true
28+
29+
- match: { status: 400 }
30+
- match: { error.type: action_request_validation_exception }
31+
- match: { error.reason: "Validation Failed: 1: cannot set only_expunge_deletes and max_num_segments at the same time, those two parameters are mutually exclusive;" }

server/src/main/java/org/elasticsearch/action/admin/indices/forcemerge/ForceMergeRequest.java

+13
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919

2020
package org.elasticsearch.action.admin.indices.forcemerge;
2121

22+
import org.elasticsearch.action.ActionRequestValidationException;
2223
import org.elasticsearch.action.support.broadcast.BroadcastRequest;
2324
import org.elasticsearch.common.io.stream.StreamInput;
2425
import org.elasticsearch.common.io.stream.StreamOutput;
2526

2627
import java.io.IOException;
2728

29+
import static org.elasticsearch.action.ValidateActions.addValidationError;
30+
2831
/**
2932
* A request to force merging the segments of one or more indices. In order to
3033
* run a merge on all the indices, pass an empty array or {@code null} for the
@@ -122,6 +125,16 @@ public void writeTo(StreamOutput out) throws IOException {
122125
out.writeBoolean(flush);
123126
}
124127

128+
@Override
129+
public ActionRequestValidationException validate() {
130+
ActionRequestValidationException validationError = super.validate();
131+
if (onlyExpungeDeletes && maxNumSegments != Defaults.MAX_NUM_SEGMENTS) {
132+
validationError = addValidationError("cannot set only_expunge_deletes and max_num_segments at the same time, those two " +
133+
"parameters are mutually exclusive", validationError);
134+
}
135+
return validationError;
136+
}
137+
125138
@Override
126139
public String toString() {
127140
return "ForceMergeRequest{" +

server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java

+3
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,9 @@ final Map<BytesRef, VersionValue> getVersionMap() {
18951895
@Override
18961896
public void forceMerge(final boolean flush, int maxNumSegments, boolean onlyExpungeDeletes,
18971897
final boolean upgrade, final boolean upgradeOnlyAncientSegments) throws EngineException, IOException {
1898+
if (onlyExpungeDeletes && maxNumSegments >= 0) {
1899+
throw new IllegalArgumentException("only_expunge_deletes and max_num_segments are mutually exclusive");
1900+
}
18981901
/*
18991902
* We do NOT acquire the readlock here since we are waiting on the merges to finish
19001903
* that's fine since the IW.rollback should stop all the threads and trigger an IOException
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.action.admin.indices.forcemerge;
20+
21+
import org.elasticsearch.action.ActionRequestValidationException;
22+
import org.elasticsearch.test.ESTestCase;
23+
24+
import static org.hamcrest.Matchers.contains;
25+
import static org.hamcrest.Matchers.equalTo;
26+
import static org.hamcrest.Matchers.notNullValue;
27+
import static org.hamcrest.Matchers.nullValue;
28+
29+
public class ForceMergeRequestTests extends ESTestCase {
30+
31+
public void testValidate() {
32+
final boolean flush = randomBoolean();
33+
final boolean onlyExpungeDeletes = randomBoolean();
34+
final int maxNumSegments = randomIntBetween(ForceMergeRequest.Defaults.MAX_NUM_SEGMENTS, 100);
35+
36+
final ForceMergeRequest request = new ForceMergeRequest();
37+
request.flush(flush);
38+
request.onlyExpungeDeletes(onlyExpungeDeletes);
39+
request.maxNumSegments(maxNumSegments);
40+
41+
assertThat(request.flush(), equalTo(flush));
42+
assertThat(request.onlyExpungeDeletes(), equalTo(onlyExpungeDeletes));
43+
assertThat(request.maxNumSegments(), equalTo(maxNumSegments));
44+
45+
ActionRequestValidationException validation = request.validate();
46+
if (onlyExpungeDeletes && maxNumSegments != ForceMergeRequest.Defaults.MAX_NUM_SEGMENTS) {
47+
assertThat(validation, notNullValue());
48+
assertThat(validation.validationErrors(), contains("cannot set only_expunge_deletes and max_num_segments at the "
49+
+ "same time, those two parameters are mutually exclusive"));
50+
} else {
51+
assertThat(validation, nullValue());
52+
}
53+
}
54+
}

server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java

+16-8
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
import static org.hamcrest.CoreMatchers.sameInstance;
190190
import static org.hamcrest.Matchers.contains;
191191
import static org.hamcrest.Matchers.containsInAnyOrder;
192+
import static org.hamcrest.Matchers.containsString;
192193
import static org.hamcrest.Matchers.empty;
193194
import static org.hamcrest.Matchers.equalTo;
194195
import static org.hamcrest.Matchers.everyItem;
@@ -197,6 +198,7 @@
197198
import static org.hamcrest.Matchers.hasItem;
198199
import static org.hamcrest.Matchers.hasKey;
199200
import static org.hamcrest.Matchers.hasSize;
201+
import static org.hamcrest.Matchers.is;
200202
import static org.hamcrest.Matchers.isIn;
201203
import static org.hamcrest.Matchers.lessThanOrEqualTo;
202204
import static org.hamcrest.Matchers.not;
@@ -1225,8 +1227,7 @@ public void testRenewSyncFlush() throws Exception {
12251227
Engine.SyncedFlushResult.SUCCESS);
12261228
assertEquals(3, engine.segments(false).size());
12271229

1228-
engine.forceMerge(forceMergeFlushes, 1, false,
1229-
false, false);
1230+
engine.forceMerge(forceMergeFlushes, 1, false, false, false);
12301231
if (forceMergeFlushes == false) {
12311232
engine.refresh("make all segments visible");
12321233
assertEquals(4, engine.segments(false).size());
@@ -1471,7 +1472,7 @@ public void testForceMergeWithoutSoftDeletes() throws IOException {
14711472
Engine.Index index = indexForDoc(doc);
14721473
engine.delete(new Engine.Delete(index.type(), index.id(), index.uid(), primaryTerm.get()));
14731474
//expunge deletes
1474-
engine.forceMerge(true, 10, true, false, false);
1475+
engine.forceMerge(true, -1, true, false, false);
14751476
engine.refresh("test");
14761477

14771478
assertEquals(engine.segments(true).size(), 1);
@@ -1752,8 +1753,7 @@ public void run() {
17521753
engine.refresh("test");
17531754
indexed.countDown();
17541755
try {
1755-
engine.forceMerge(randomBoolean(), 1, false, randomBoolean(),
1756-
randomBoolean());
1756+
engine.forceMerge(randomBoolean(), 1, false, randomBoolean(), randomBoolean());
17571757
} catch (IOException e) {
17581758
return;
17591759
}
@@ -3162,8 +3162,7 @@ public void run() {
31623162
try {
31633163
switch (operation) {
31643164
case "optimize": {
3165-
engine.forceMerge(true, 1, false, false,
3166-
false);
3165+
engine.forceMerge(true, 1, false, false, false);
31673166
break;
31683167
}
31693168
case "refresh": {
@@ -4364,7 +4363,16 @@ public void testRandomOperations() throws Exception {
43644363
engine.flush();
43654364
}
43664365
if (randomBoolean()) {
4367-
engine.forceMerge(randomBoolean(), between(1, 10), randomBoolean(), false, false);
4366+
boolean flush = randomBoolean();
4367+
boolean onlyExpungeDeletes = randomBoolean();
4368+
int maxNumSegments = randomIntBetween(-1, 10);
4369+
try {
4370+
engine.forceMerge(flush, maxNumSegments, onlyExpungeDeletes, false, false);
4371+
} catch (IllegalArgumentException e) {
4372+
assertThat(e.getMessage(), containsString("only_expunge_deletes and max_num_segments are mutually exclusive"));
4373+
assertThat(onlyExpungeDeletes, is(true));
4374+
assertThat(maxNumSegments, greaterThan(-1));
4375+
}
43684376
}
43694377
}
43704378
if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) {

0 commit comments

Comments
 (0)