Skip to content

Commit 55fa131

Browse files
authored
Update requests on mixed cluster should not use ifSeqNo(#42596)
ifSeqNo feature requires all nodes are on version 6.6.0 or higher. However, we don't check this requirement until we serialize index/update requests. If the primary is on 6.6+ and replicas on an old version, then we fail to execute update requests (with ifSeqNo or with version or no CAS) because we always use ifSeqNo in this situation. With this change, we use ifSeqNo only if all nodes are on 6.6+; otherwise, we fall back to use version for update requests. Closes #42561
1 parent 255e82d commit 55fa131

File tree

6 files changed

+109
-8
lines changed

6 files changed

+109
-8
lines changed

qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,24 @@ public void testRecoveryWithSoftDeletes() throws Exception {
430430
ensureGreen(index);
431431
}
432432

433+
/** Ensure that we can always execute update requests regardless of the version of cluster */
434+
public void testUpdateDoc() throws Exception {
435+
final String index = "test_update_doc";
436+
if (CLUSTER_TYPE == ClusterType.OLD) {
437+
Settings.Builder settings = Settings.builder()
438+
.put(IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1)
439+
.put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 2);
440+
createIndex(index, settings.build());
441+
}
442+
ensureGreen(index);
443+
indexDocs(index, 0, 10);
444+
for (int i = 0; i < 10; i++) {
445+
Request update = new Request("POST", index + "/test/" + i + "/_update");
446+
update.setJsonEntity("{\"doc\": {\"f\": " + randomNonNegativeLong() + "}}");
447+
client().performRequest(update);
448+
}
449+
}
450+
433451
private void syncedFlush(String index) throws Exception {
434452
// We have to spin synced-flush requests here because we fire the global checkpoint sync for the last write operation.
435453
// A synced-flush request considers the global checkpoint sync as an going operation because it acquires a shard permit.

server/src/main/java/org/elasticsearch/action/delete/DeleteRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public void writeTo(StreamOutput out) throws IOException {
288288
} else if (ifSeqNo != UNASSIGNED_SEQ_NO || ifPrimaryTerm != UNASSIGNED_PRIMARY_TERM) {
289289
assert false : "setIfMatch [" + ifSeqNo + "], currentDocTem [" + ifPrimaryTerm + "]";
290290
throw new IllegalStateException(
291-
"sequence number based compare and write is not supported until all nodes are on version 7.0 or higher. " +
291+
"sequence number based compare and write is not supported until all nodes are on version 6.6.0 or higher. " +
292292
"Stream version [" + out.getVersion() + "]");
293293
}
294294
}

server/src/main/java/org/elasticsearch/action/index/IndexRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ public void writeTo(StreamOutput out) throws IOException {
656656
} else if (ifSeqNo != UNASSIGNED_SEQ_NO || ifPrimaryTerm != UNASSIGNED_PRIMARY_TERM) {
657657
assert false : "setIfMatch [" + ifSeqNo + "], currentDocTem [" + ifPrimaryTerm + "]";
658658
throw new IllegalStateException(
659-
"sequence number based compare and write is not supported until all nodes are on version 7.0 or higher. " +
659+
"sequence number based compare and write is not supported until all nodes are on version 6.6.0 or higher. " +
660660
"Stream version [" + out.getVersion() + "]");
661661
}
662662
}

server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import org.apache.logging.log4j.LogManager;
2323
import org.apache.logging.log4j.Logger;
2424
import org.elasticsearch.ElasticsearchException;
25+
import org.elasticsearch.Version;
2526
import org.elasticsearch.action.DocWriteResponse;
2627
import org.elasticsearch.action.delete.DeleteRequest;
2728
import org.elasticsearch.action.index.IndexRequest;
2829
import org.elasticsearch.client.Requests;
30+
import org.elasticsearch.cluster.service.ClusterService;
2931
import org.elasticsearch.common.Nullable;
3032
import org.elasticsearch.common.bytes.BytesReference;
3133
import org.elasticsearch.common.collect.Tuple;
@@ -41,6 +43,7 @@
4143
import org.elasticsearch.index.get.GetResult;
4244
import org.elasticsearch.index.mapper.ParentFieldMapper;
4345
import org.elasticsearch.index.mapper.RoutingFieldMapper;
46+
import org.elasticsearch.index.seqno.SequenceNumbers;
4447
import org.elasticsearch.index.shard.IndexShard;
4548
import org.elasticsearch.index.shard.ShardId;
4649
import org.elasticsearch.script.Script;
@@ -52,6 +55,7 @@
5255
import java.util.ArrayList;
5356
import java.util.HashMap;
5457
import java.util.Map;
58+
import java.util.function.BooleanSupplier;
5559
import java.util.function.LongSupplier;
5660

5761
/**
@@ -62,15 +66,24 @@ public class UpdateHelper {
6266
private static final Logger logger = LogManager.getLogger(UpdateHelper.class);
6367

6468
private final ScriptService scriptService;
69+
private final BooleanSupplier canUseIfSeqNo;
6570

66-
public UpdateHelper(ScriptService scriptService) {
71+
public UpdateHelper(ScriptService scriptService, ClusterService clusterService) {
72+
this(scriptService, () -> clusterService.state().nodes().getMinNodeVersion().onOrAfter(Version.V_6_6_0));
73+
}
74+
75+
UpdateHelper(ScriptService scriptService, BooleanSupplier canUseIfSeqNo) {
6776
this.scriptService = scriptService;
77+
this.canUseIfSeqNo = canUseIfSeqNo;
6878
}
6979

7080
/**
7181
* Prepares an update request by converting it into an index or delete request or an update response (no action).
7282
*/
7383
public Result prepare(UpdateRequest request, IndexShard indexShard, LongSupplier nowInMillis) {
84+
if (canUseIfSeqNo.getAsBoolean() == false) {
85+
ensureIfSeqNoNotProvided(request.ifSeqNo(), request.ifPrimaryTerm());
86+
}
7487
final GetResult getResult = indexShard.getService().getForUpdate(
7588
request.type(), request.id(), request.version(), request.versionType(), request.ifSeqNo(), request.ifPrimaryTerm());
7689
return prepare(indexShard.shardId(), request, getResult, nowInMillis);
@@ -165,6 +178,19 @@ Result prepareUpsert(ShardId shardId, UpdateRequest request, final GetResult get
165178
return new Result(indexRequest, DocWriteResponse.Result.CREATED, null, null);
166179
}
167180

181+
/**
182+
* Calculate the version to use for the update request, using either the existing version if internal versioning is used, or the get
183+
* result document's version if the version type is "FORCE".
184+
*/
185+
static long calculateUpdateVersion(UpdateRequest request, GetResult getResult) {
186+
if (request.versionType() != VersionType.INTERNAL) {
187+
assert request.versionType() == VersionType.FORCE;
188+
return request.version(); // remember, match_any is excluded by the conflict test
189+
} else {
190+
return getResult.getVersion();
191+
}
192+
}
193+
168194
/**
169195
* Calculate a routing value to be used, either the included index request's routing, or retrieved document's routing when defined.
170196
*/
@@ -219,9 +245,13 @@ Result prepareUpdateIndexRequest(ShardId shardId, UpdateRequest request, GetResu
219245
final IndexRequest finalIndexRequest = Requests.indexRequest(request.index())
220246
.type(request.type()).id(request.id()).routing(routing).parent(parent)
221247
.source(updatedSourceAsMap, updateSourceContentType)
222-
.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm())
223248
.waitForActiveShards(request.waitForActiveShards()).timeout(request.timeout())
224249
.setRefreshPolicy(request.getRefreshPolicy());
250+
if (canUseIfSeqNo.getAsBoolean()) {
251+
finalIndexRequest.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm());
252+
} else {
253+
finalIndexRequest.version(calculateUpdateVersion(request, getResult)).versionType(request.versionType());
254+
}
225255
return new Result(finalIndexRequest, DocWriteResponse.Result.UPDATED, updatedSourceAsMap, updateSourceContentType);
226256
}
227257
}
@@ -261,16 +291,24 @@ Result prepareUpdateScriptRequest(ShardId shardId, UpdateRequest request, GetRes
261291
final IndexRequest indexRequest = Requests.indexRequest(request.index())
262292
.type(request.type()).id(request.id()).routing(routing).parent(parent)
263293
.source(updatedSourceAsMap, updateSourceContentType)
264-
.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm())
265294
.waitForActiveShards(request.waitForActiveShards()).timeout(request.timeout())
266295
.setRefreshPolicy(request.getRefreshPolicy());
296+
if (canUseIfSeqNo.getAsBoolean()) {
297+
indexRequest.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm());
298+
} else {
299+
indexRequest.version(calculateUpdateVersion(request, getResult)).versionType(request.versionType());
300+
}
267301
return new Result(indexRequest, DocWriteResponse.Result.UPDATED, updatedSourceAsMap, updateSourceContentType);
268302
case DELETE:
269303
DeleteRequest deleteRequest = Requests.deleteRequest(request.index())
270304
.type(request.type()).id(request.id()).routing(routing).parent(parent)
271-
.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm())
272305
.waitForActiveShards(request.waitForActiveShards())
273306
.timeout(request.timeout()).setRefreshPolicy(request.getRefreshPolicy());
307+
if (canUseIfSeqNo.getAsBoolean()) {
308+
deleteRequest.setIfSeqNo(getResult.getSeqNo()).setIfPrimaryTerm(getResult.getPrimaryTerm());
309+
} else {
310+
deleteRequest.version(calculateUpdateVersion(request, getResult)).versionType(request.versionType());
311+
}
274312
return new Result(deleteRequest, DocWriteResponse.Result.DELETED, updatedSourceAsMap, updateSourceContentType);
275313
default:
276314
// If it was neither an INDEX or DELETE operation, treat it as a noop
@@ -354,6 +392,14 @@ public static GetResult extractGetResult(final UpdateRequest request, String con
354392
sourceRequested ? sourceFilteredAsBytes : null, fields);
355393
}
356394

395+
private void ensureIfSeqNoNotProvided(long ifSeqNo, long ifPrimaryTerm) {
396+
if (ifSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO || ifPrimaryTerm != SequenceNumbers.UNASSIGNED_PRIMARY_TERM) {
397+
assert false : "setIfMatch [" + ifSeqNo + "], currentDocTem [" + ifPrimaryTerm + "]";
398+
throw new IllegalStateException(
399+
"sequence number based compare and write is not supported until all nodes are on version 6.6.0 or higher.");
400+
}
401+
}
402+
357403
public static class Result {
358404

359405
private final Streamable action;

server/src/main/java/org/elasticsearch/node/Node.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ protected Node(
582582
b.bind(Transport.class).toInstance(transport);
583583
b.bind(TransportService.class).toInstance(transportService);
584584
b.bind(NetworkService.class).toInstance(networkService);
585-
b.bind(UpdateHelper.class).toInstance(new UpdateHelper(scriptModule.getScriptService()));
585+
b.bind(UpdateHelper.class).toInstance(new UpdateHelper(scriptModule.getScriptService(), clusterService));
586586
b.bind(MetaDataIndexUpgradeService.class).toInstance(metaDataIndexUpgradeService);
587587
b.bind(ClusterInfoService.class).toInstance(clusterInfoService);
588588
b.bind(GatewayMetaState.class).toInstance(gatewayMetaState);

server/src/test/java/org/elasticsearch/action/update/UpdateRequestTests.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.common.bytes.BytesReference;
2929
import org.elasticsearch.common.document.DocumentField;
3030
import org.elasticsearch.common.io.stream.Streamable;
31+
import org.elasticsearch.common.lucene.uid.Versions;
3132
import org.elasticsearch.common.settings.Settings;
3233
import org.elasticsearch.common.xcontent.ToXContent;
3334
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -53,11 +54,13 @@
5354
import java.util.Collections;
5455
import java.util.HashMap;
5556
import java.util.Map;
57+
import java.util.concurrent.atomic.AtomicBoolean;
5658
import java.util.function.Function;
5759

5860
import static java.util.Collections.emptyMap;
5961
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
6062
import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
63+
import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM;
6164
import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO;
6265
import static org.elasticsearch.script.MockScriptEngine.mockInlineScript;
6366
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
@@ -74,6 +77,7 @@
7477
public class UpdateRequestTests extends ESTestCase {
7578

7679
private UpdateHelper updateHelper;
80+
private final AtomicBoolean canUseIfSeqNo = new AtomicBoolean(true);
7781

7882
@Override
7983
@Before
@@ -139,7 +143,7 @@ public void setUp() throws Exception {
139143
final MockScriptEngine engine = new MockScriptEngine("mock", scripts, Collections.emptyMap());
140144
Map<String, ScriptEngine> engines = Collections.singletonMap(engine.getType(), engine);
141145
ScriptService scriptService = new ScriptService(baseSettings, engines, ScriptModule.CORE_CONTEXTS);
142-
updateHelper = new UpdateHelper(scriptService);
146+
updateHelper = new UpdateHelper(scriptService, canUseIfSeqNo::get);
143147
}
144148

145149
public void testFromXContent() throws Exception {
@@ -696,6 +700,39 @@ public void testUpdateScript() throws Exception {
696700
assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.NOOP));
697701
}
698702

703+
public void testOldClusterFallbackToUseVersion() throws Exception {
704+
ShardId shardId = new ShardId("test", "", 0);
705+
long version = randomNonNegativeLong();
706+
long seqNo = randomNonNegativeLong();
707+
long primaryTerm = randomNonNegativeLong();
708+
GetResult getResult = new GetResult("test", "type", "1", seqNo, primaryTerm, version, true,
709+
new BytesArray("{\"body\": \"bar\"}"), null);
710+
UpdateRequest request = new UpdateRequest("test", "type1", "1").fromXContent(
711+
createParser(JsonXContent.jsonXContent, new BytesArray("{\"doc\": {\"body\": \"foo\"}}")));
712+
713+
canUseIfSeqNo.set(false);
714+
IndexRequest updateUsingVersion = updateHelper.prepare(shardId, request, getResult, ESTestCase::randomNonNegativeLong).action();
715+
assertThat(updateUsingVersion.ifSeqNo(), equalTo(UNASSIGNED_SEQ_NO));
716+
assertThat(updateUsingVersion.ifPrimaryTerm(), equalTo(UNASSIGNED_PRIMARY_TERM));
717+
assertThat(updateUsingVersion.version(), equalTo(version));
718+
719+
canUseIfSeqNo.set(true);
720+
IndexRequest updateUsingSeqNo = updateHelper.prepare(shardId, request, getResult, ESTestCase::randomNonNegativeLong).action();
721+
assertThat(updateUsingSeqNo.ifSeqNo(), equalTo(seqNo));
722+
assertThat(updateUsingSeqNo.ifPrimaryTerm(), equalTo(primaryTerm));
723+
assertThat(updateUsingSeqNo.version(), equalTo(Versions.MATCH_ANY));
724+
}
725+
726+
public void testOldClusterRejectIfSeqNo() {
727+
canUseIfSeqNo.set(false);
728+
long ifSeqNo = randomNonNegativeLong();
729+
long ifPrimaryTerm = randomNonNegativeLong();
730+
UpdateRequest request = new UpdateRequest("test", "type1", "1").setIfSeqNo(ifSeqNo).setIfPrimaryTerm(ifPrimaryTerm);
731+
AssertionError error = expectThrows(AssertionError.class,
732+
() -> updateHelper.prepare(request, null, ESTestCase::randomNonNegativeLong));
733+
assertThat(error.getMessage(), equalTo("setIfMatch [" + ifSeqNo + "], currentDocTem [" + ifPrimaryTerm + "]"));
734+
}
735+
699736
public void testToString() throws IOException {
700737
UpdateRequest request = new UpdateRequest("test", "type1", "1")
701738
.script(mockInlineScript("ctx._source.body = \"foo\""));

0 commit comments

Comments
 (0)