Skip to content

Commit fdd4133

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 dfc308a commit fdd4133

File tree

4 files changed

+49
-24
lines changed

4 files changed

+49
-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
@@ -1032,7 +1032,7 @@ private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException {
10321032
currentVersion = versionValue.version;
10331033
currentNotFoundOrDeleted = versionValue.isDelete();
10341034
}
1035-
if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) {
1035+
if (index.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentNotFoundOrDeleted) {
10361036
final VersionConflictEngineException e = new VersionConflictEngineException(shardId, index.id(),
10371037
index.getIfSeqNo(), index.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO,
10381038
SequenceNumbers.UNASSIGNED_PRIMARY_TERM);
@@ -1361,7 +1361,7 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException
13611361
currentlyDeleted = versionValue.isDelete();
13621362
}
13631363
final DeletionStrategy plan;
1364-
if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && versionValue == null) {
1364+
if (delete.getIfSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && currentlyDeleted) {
13651365
final VersionConflictEngineException e = new VersionConflictEngineException(shardId, delete.id(),
13661366
delete.getIfSeqNo(), delete.getIfPrimaryTerm(), SequenceNumbers.UNASSIGNED_SEQ_NO, SequenceNumbers.UNASSIGNED_PRIMARY_TERM);
13671367
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
@@ -138,6 +138,7 @@
138138
import org.elasticsearch.test.VersionUtils;
139139
import org.elasticsearch.threadpool.ThreadPool;
140140
import org.hamcrest.MatcherAssert;
141+
import org.hamcrest.Matchers;
141142

142143
import java.io.Closeable;
143144
import java.io.IOException;
@@ -1719,7 +1720,7 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
17191720
currentTerm.set(currentTerm.get() + 1L);
17201721
engine.rollTranslogGeneration();
17211722
}
1722-
final long correctVersion = docDeleted && randomBoolean() ? Versions.MATCH_DELETED : lastOpVersion;
1723+
final long correctVersion = docDeleted ? Versions.MATCH_DELETED : lastOpVersion;
17231724
logger.info("performing [{}]{}{}",
17241725
op.operationType().name().charAt(0),
17251726
versionConflict ? " (conflict " + conflictingVersion + ")" : "",
@@ -1742,7 +1743,7 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
17421743
final Engine.IndexResult result;
17431744
if (versionedOp) {
17441745
// TODO: add support for non-existing docs
1745-
if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO) {
1746+
if (randomBoolean() && lastOpSeqNo != SequenceNumbers.UNASSIGNED_SEQ_NO && docDeleted == false) {
17461747
result = engine.index(indexWithSeq.apply(lastOpSeqNo, lastOpTerm, index));
17471748
} else {
17481749
result = engine.index(indexWithVersion.apply(correctVersion, index));
@@ -1777,8 +1778,9 @@ private int assertOpsOnPrimary(List<Engine.Operation> ops, long currentOpVersion
17771778
assertThat(result.getFailure(), instanceOf(VersionConflictEngineException.class));
17781779
} else {
17791780
final Engine.DeleteResult result;
1781+
long correctSeqNo = docDeleted ? UNASSIGNED_SEQ_NO : lastOpSeqNo;
17801782
if (versionedOp && lastOpSeqNo != UNASSIGNED_SEQ_NO && randomBoolean()) {
1781-
result = engine.delete(delWithSeq.apply(lastOpSeqNo, lastOpTerm, delete));
1783+
result = engine.delete(delWithSeq.apply(correctSeqNo, lastOpTerm, delete));
17821784
} else if (versionedOp) {
17831785
result = engine.delete(delWithVersion.apply(correctVersion, delete));
17841786
} else {
@@ -4031,6 +4033,36 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio
40314033
}
40324034
}
40334035

4036+
/**
4037+
* 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:
4038+
* <ul>
4039+
* <li>Guessing the deleted seqNo makes the operation succeed</li>
4040+
* <li>Providing any other seqNo leaks info that the doc was deleted (and its SeqNo)</li>
4041+
* </ul>
4042+
*/
4043+
public void testVersionConflictIgnoreDeletedDoc() throws IOException {
4044+
ParsedDocument doc = testParsedDocument("1", null, testDocument(),
4045+
new BytesArray("{}".getBytes(Charset.defaultCharset())), null);
4046+
engine.delete(new Engine.Delete("1", newUid("1"), 1));
4047+
for (long seqNo : new long[]{0, 1, randomNonNegativeLong()}) {
4048+
assertDeletedVersionConflict(engine.index(new Engine.Index(newUid("1"), doc, UNASSIGNED_SEQ_NO, 1,
4049+
Versions.MATCH_ANY, VersionType.INTERNAL,
4050+
PRIMARY, randomNonNegativeLong(), IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, seqNo, 1)),
4051+
"update: " + seqNo);
4052+
4053+
assertDeletedVersionConflict(engine.delete(new Engine.Delete("1", newUid("1"), UNASSIGNED_SEQ_NO, 1,
4054+
Versions.MATCH_ANY, VersionType.INTERNAL, PRIMARY, randomNonNegativeLong(), seqNo, 1)),
4055+
"delete: " + seqNo);
4056+
}
4057+
}
4058+
4059+
private void assertDeletedVersionConflict(Engine.Result result, String operation) {
4060+
assertNotNull("Must have failure for " + operation, result.getFailure());
4061+
assertThat(operation, result.getFailure(), Matchers.instanceOf(VersionConflictEngineException.class));
4062+
VersionConflictEngineException exception = (VersionConflictEngineException) result.getFailure();
4063+
assertThat(operation, exception.getMessage(), containsString("but no document was found"));
4064+
}
4065+
40344066
/*
40354067
* 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
40364068
* are correctly added to the translog.

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

Lines changed: 10 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,24 @@ public void testOpenCloseUpdateSettings() throws Exception {
449449

450450
public void testEngineGCDeletesSetting() throws Exception {
451451
createIndex("test");
452-
client().prepareIndex("test").setId("1").setSource("f", 1).get();
453-
DeleteResponse response = client().prepareDelete("test", "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").setId("1").setSource("f", 2).setIfSeqNo(seqNo).setIfPrimaryTerm(primaryTerm).get();
452+
client().prepareIndex("test").setId("1").setSource("f", 1).setVersionType(VersionType.EXTERNAL).setVersion(1).get();
453+
client().prepareDelete("test", "1").setVersionType(VersionType.EXTERNAL).setVersion(2).get();
454+
// delete is still in cache this should fail
455+
assertThrows(client().prepareIndex("test").setId("1").setSource("f", 3).setVersionType(VersionType.EXTERNAL).setVersion(1),
456+
VersionConflictEngineException.class);
457+
458458
assertAcked(client().admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder().put("index.gc_deletes", 0)));
459459

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

463462
// Make sure the time has advanced for InternalEngine#resolveDocVersion()
464463
for (ThreadPool threadPool : internalCluster().getInstances(ThreadPool.class)) {
465464
long startTime = threadPool.relativeTimeInMillis();
466465
assertBusy(() -> assertThat(threadPool.relativeTimeInMillis(), greaterThan(startTime)));
467466
}
468467

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

474472
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", "1").setIfSeqNo(3).setIfPrimaryTerm(12), VersionConflictEngineException.class);
284284
assertThrows(client().prepareDelete("test", "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", "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", "1").setIfSeqNo(2).setIfPrimaryTerm(1), VersionConflictEngineException.class);
293288
}
294289

295290
public void testSimpleVersioningWithFlush() throws Exception {

0 commit comments

Comments
 (0)