Skip to content

Commit ec0ec61

Browse files
Deleted docs disregarded for if_seq_no check (#50526)
Previously, as long as a deleted version value was kept as a tombstone, another index or delete operation against the same id would leak that the doc had existed (through seq_no info) or would allow the operation if the client forged the seq_no. Fixed to disregard info on deleted docs when doing seq_no based optimistic concurrency check.
1 parent 5533e11 commit ec0ec61

File tree

4 files changed

+48
-24
lines changed

4 files changed

+48
-24
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException {
10431043
currentVersion = versionValue.version;
10441044
currentNotFoundOrDeleted = versionValue.isDelete();
10451045
}
1046-
if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) {
1046+
if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentNotFoundOrDeleted) {
10471047
final VersionConflictEngineException e = new VersionConflictEngineException(shardId, index.id(),
10481048
index.getIfSeqNo(), index.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO,
10491049
SequenceNumbers.UNASSIGNED_PRIMARY_TERM);
@@ -1376,7 +1376,7 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException
13761376
currentlyDeleted = versionValue.isDelete();
13771377
}
13781378
final DeletionStrategy plan;
1379-
if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) {
1379+
if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentlyDeleted) {
13801380
final VersionConflictEngineException e = new VersionConflictEngineException(shardId, delete.id(),
13811381
delete.getIfSeqNo(), delete.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO, SequenceNumbers.UNASSIGNED_PRIMARY_TERM);
13821382
plan = DeletionStrategy.skipDueToVersionConflict(e, currentVersion, true);

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
import org.elasticsearch.test.VersionUtils;
144144
import org.elasticsearch.threadpool.ThreadPool;
145145
import org.hamcrest.MatcherAssert;
146+
import org.hamcrest.Matchers;
146147

147148
import java.io.Closeable;
148149
import java.io.IOException;
@@ -1979,7 +1980,7 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
19791980
currentTerm.set(currentTerm.get() + 1L);
19801981
engine.rollTranslogGeneration();
19811982
}
1982-
final long correctVersion = docDeleted && randomBoolean() ? Versions.MATCH_DELETED : lastOpVersion;
1983+
final long correctVersion = docDeleted ? Versions.MATCH_DELETED : lastOpVersion;
19831984
logger.info("performing [{}]{}{}",
19841985
op.operationType().name().charAt(0),
19851986
versionConflict ? " (conflict " + conflictingVersion + ")" : "",
@@ -2002,7 +2003,7 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
20022003
final Engine.IndexResult result;
20032004
if (versionedOp) {
20042005
// TODO: add support for non-existing docs
2005-
if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO) {
2006+
if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO && docDeleted == false) {
20062007
result = engine.index(indexWithSeq.apply(lastOpSeqNo, lastOpTerm, index));
20072008
} else {
20082009
result = engine.index(indexWithVersion.apply(correctVersion, index));
@@ -2037,8 +2038,9 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
20372038
assertThat(result.getFailure(), instanceOf(VersionConflictEngineException.class));
20382039
} else {
20392040
final Engine.DeleteResult result;
2041+
long correctSeqNo = docDeleted ? UNASSIGNED_SEQ_NO : lastOpSeqNo;
20402042
if (versionedOp && lastOpSeqNo != UNASSIGNED_SEQ_NO && randomBoolean()) {
2041-
result = engine.delete(delWithSeq.apply(lastOpSeqNo, lastOpTerm, delete));
2043+
result = engine.delete(delWithSeq.apply(correctSeqNo, lastOpTerm, delete));
20422044
} else if (versionedOp) {
20432045
result = engine.delete(delWithVersion.apply(correctVersion, delete));
20442046
} else {
@@ -4312,6 +4314,36 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio
43124314
}
43134315
}
43144316

4317+
/**
4318+
* Test that we do not leak out information on a deleted doc due to it existing in version map. There are at least 2 cases:
4319+
* <ul>
4320+
* <li>Guessing the deleted seqNo makes the operation succeed</li>
4321+
* <li>Providing any other seqNo leaks info that the doc was deleted (and its SeqNo)</li>
4322+
* </ul>
4323+
*/
4324+
public void testVersionConflictIgnoreDeletedDoc() throws IOException {
4325+
ParsedDocument doc = testParsedDocument("1", null, testDocument(),
4326+
new BytesArray("{}".getBytes(Charset.defaultCharset())), null);
4327+
engine.delete(new Engine.Delete("test", "1", newUid("1"), 1));
4328+
for (long seqNo : new long[]{0, 1, randomNonNegativeLong()}) {
4329+
assertDeletedVersionConflict(engine.index(new Engine.Index(newUid("1"), doc, UNASSIGNED_SEQ_NO, 1,
4330+
Versions.MATCH_ANY, VersionType.INTERNAL,
4331+
PRIMARY, randomNonNegativeLong(), IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, seqNo, 1)),
4332+
"update: " + seqNo);
4333+
4334+
assertDeletedVersionConflict(engine.delete(new Engine.Delete("test", "1", newUid("1"), UNASSIGNED_SEQ_NO, 1,
4335+
Versions.MATCH_ANY, VersionType.INTERNAL, PRIMARY, randomNonNegativeLong(), seqNo, 1)),
4336+
"delete: " + seqNo);
4337+
}
4338+
}
4339+
4340+
private void assertDeletedVersionConflict(Engine.Result result, String operation) {
4341+
assertNotNull("Must have failure for " + operation, result.getFailure());
4342+
assertThat(operation, result.getFailure(), Matchers.instanceOf(VersionConflictEngineException.class));
4343+
VersionConflictEngineException exception = (VersionConflictEngineException) result.getFailure();
4344+
assertThat(operation, exception.getMessage(), containsString("but no document was found"));
4345+
}
4346+
43154347
/*
43164348
* This test tests that a no-op does not generate a new sequence number, that no-ops can advance the local checkpoint, and that no-ops
43174349
* are correctly added to the translog.

server/src/test/java/org/elasticsearch/indices/settings/UpdateSettingsIT.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121

2222
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
2323
import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
24-
import org.elasticsearch.action.delete.DeleteResponse;
2524
import org.elasticsearch.cluster.metadata.IndexMetaData;
2625
import org.elasticsearch.common.Priority;
2726
import org.elasticsearch.common.settings.Setting;
2827
import org.elasticsearch.common.settings.Settings;
2928
import org.elasticsearch.index.IndexModule;
3029
import org.elasticsearch.index.IndexService;
30+
import org.elasticsearch.index.VersionType;
3131
import org.elasticsearch.index.engine.VersionConflictEngineException;
3232
import org.elasticsearch.indices.IndicesService;
3333
import org.elasticsearch.plugins.Plugin;
@@ -449,26 +449,23 @@ public void testOpenCloseUpdateSettings() throws Exception {
449449

450450
public void testEngineGCDeletesSetting() throws Exception {
451451
createIndex("test");
452-
client().prepareIndex("test", "type", "1").setSource("f", 1).get();
453-
DeleteResponse response = client().prepareDelete("test", "type", "1").get();
454-
long seqNo = response.getSeqNo();
455-
long primaryTerm = response.getPrimaryTerm();
456-
// delete is still in cache this should work
457-
client().prepareIndex("test", "type", "1").setSource("f", 2).setIfSeqNo(seqNo).setIfPrimaryTerm(primaryTerm).get();
452+
client().prepareIndex("test", "type", "1").setSource("f", 1).setVersionType(VersionType.EXTERNAL).setVersion(1).get();
453+
client().prepareDelete("test", "type", "1").setVersionType(VersionType.EXTERNAL).setVersion(2).get();
454+
// delete is still in cache this should fail
455+
assertThrows(client().prepareIndex("test", "type", "1").setSource("f", 3).setVersionType(VersionType.EXTERNAL).setVersion(1),
456+
VersionConflictEngineException.class);
458457
assertAcked(client().admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder().put("index.gc_deletes", 0)));
459458

460-
response = client().prepareDelete("test", "type", "1").get();
461-
seqNo = response.getSeqNo();
459+
client().prepareDelete("test", "type", "1").setVersionType(VersionType.EXTERNAL).setVersion(4).get();
462460

463461
// Make sure the time has advanced for InternalEngine#resolveDocVersion()
464462
for (ThreadPool threadPool : internalCluster().getInstances(ThreadPool.class)) {
465463
long startTime = threadPool.relativeTimeInMillis();
466464
assertBusy(() -> assertThat(threadPool.relativeTimeInMillis(), greaterThan(startTime)));
467465
}
468466

469-
// delete is should not be in cache
470-
assertThrows(client().prepareIndex("test", "type", "1").setSource("f", 3).setIfSeqNo(seqNo).setIfPrimaryTerm(primaryTerm),
471-
VersionConflictEngineException.class);
467+
// delete should not be in cache
468+
client().prepareIndex("test", "type", "1").setSource("f", 2).setVersionType(VersionType.EXTERNAL).setVersion(1);
472469
}
473470

474471
public void testUpdateSettingsWithBlocks() {

server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,8 @@ public void testCompareAndSet() {
283283
assertThrows(client().prepareDelete("test", "type", "1").setIfSeqNo(3).setIfPrimaryTerm(12), VersionConflictEngineException.class);
284284
assertThrows(client().prepareDelete("test", "type", "1").setIfSeqNo(1).setIfPrimaryTerm(2), VersionConflictEngineException.class);
285285

286-
287-
// This is intricate - the object was deleted but a delete transaction was with the right version. We add another one
288-
// and thus the transaction is increased.
289-
deleteResponse = client().prepareDelete("test", "type", "1").setIfSeqNo(2).setIfPrimaryTerm(1).get();
290-
assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult());
291-
assertThat(deleteResponse.getSeqNo(), equalTo(3L));
292-
assertThat(deleteResponse.getPrimaryTerm(), equalTo(1L));
286+
// the doc is deleted. Even when we hit the deleted seqNo, a conditional delete should fail.
287+
assertThrows(client().prepareDelete("test", "type", "1").setIfSeqNo(2).setIfPrimaryTerm(1), VersionConflictEngineException.class);
293288
}
294289

295290
public void testSimpleVersioningWithFlush() throws Exception {

0 commit comments

Comments
 (0)