/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.versioning; import org.apache.lucene.util.TestUtil; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.test.ESIntegTestCase; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertThrows; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; public class SimpleVersioningIT extends ESIntegTestCase { public void testExternalVersioningInitialDelete() throws Exception { createIndex("test"); ensureGreen(); // Note - external version doesn't throw version conflicts on deletes of non existent records. This is different from internal versioning DeleteResponse deleteResponse = client().prepareDelete("test", "type", "1").setVersion(17).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult()); // this should conflict with the delete command transaction which told us that the object was deleted at version 17. assertThrows( client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(13).setVersionType(VersionType.EXTERNAL).execute(), VersionConflictEngineException.class ); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(18). setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(18L)); } public void testExternalGTE() throws Exception { createIndex("test"); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(12).setVersionType(VersionType.EXTERNAL_GTE).get(); assertThat(indexResponse.getVersion(), equalTo(12L)); indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_2").setVersion(12).setVersionType(VersionType.EXTERNAL_GTE).get(); assertThat(indexResponse.getVersion(), equalTo(12L)); indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_2").setVersion(14).setVersionType(VersionType.EXTERNAL_GTE).get(); assertThat(indexResponse.getVersion(), equalTo(14L)); assertThrows(client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(13).setVersionType(VersionType.EXTERNAL_GTE), VersionConflictEngineException.class); client().admin().indices().prepareRefresh().execute().actionGet(); if (randomBoolean()) { refresh(); } for (int i = 0; i < 10; i++) { assertThat(client().prepareGet("test", "type", "1").get().getVersion(), equalTo(14L)); } // deleting with a lower version fails. assertThrows( client().prepareDelete("test", "type", "1").setVersion(2).setVersionType(VersionType.EXTERNAL_GTE), VersionConflictEngineException.class); // Delete with a higher or equal version deletes all versions up to the given one. long v = randomIntBetween(14, 17); DeleteResponse deleteResponse = client().prepareDelete("test", "type", "1").setVersion(v).setVersionType(VersionType.EXTERNAL_GTE).execute().actionGet(); assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(v)); // Deleting with a lower version keeps on failing after a delete. assertThrows( client().prepareDelete("test", "type", "1").setVersion(2).setVersionType(VersionType.EXTERNAL_GTE).execute(), VersionConflictEngineException.class); // But delete with a higher version is OK. deleteResponse = client().prepareDelete("test", "type", "1").setVersion(18).setVersionType(VersionType.EXTERNAL_GTE).execute().actionGet(); assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(18L)); } public void testExternalVersioning() throws Exception { createIndex("test"); ensureGreen(); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(12).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(12L)); indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(14).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(14L)); assertThrows(client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(13).setVersionType(VersionType.EXTERNAL).execute(), VersionConflictEngineException.class); if (randomBoolean()) { refresh(); } for (int i = 0; i < 10; i++) { assertThat(client().prepareGet("test", "type", "1").execute().actionGet().getVersion(), equalTo(14L)); } // deleting with a lower version fails. assertThrows( client().prepareDelete("test", "type", "1").setVersion(2).setVersionType(VersionType.EXTERNAL).execute(), VersionConflictEngineException.class); // Delete with a higher version deletes all versions up to the given one. DeleteResponse deleteResponse = client().prepareDelete("test", "type", "1").setVersion(17).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(17L)); // Deleting with a lower version keeps on failing after a delete. assertThrows( client().prepareDelete("test", "type", "1").setVersion(2).setVersionType(VersionType.EXTERNAL).execute(), VersionConflictEngineException.class); // But delete with a higher version is OK. deleteResponse = client().prepareDelete("test", "type", "1").setVersion(18).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(18L)); // TODO: This behavior breaks rest api returning http status 201, good news is that it this is only the case until deletes GC kicks in. indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(19).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(19L)); deleteResponse = client().prepareDelete("test", "type", "1").setVersion(20).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(20L)); // Make sure that the next delete will be GC. Note we do it on the index settings so it will be cleaned up HashMap<String, Object> newSettings = new HashMap<>(); newSettings.put("index.gc_deletes", -1); client().admin().indices().prepareUpdateSettings("test").setSettings(newSettings).execute().actionGet(); Thread.sleep(300); // gc works based on estimated sampled time. Give it a chance... // And now we have previous version return -1 indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(20).setVersionType(VersionType.EXTERNAL).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(20L)); } public void testRequireUnitsOnUpdateSettings() throws Exception { createIndex("test"); ensureGreen(); HashMap<String, Object> newSettings = new HashMap<>(); newSettings.put("index.gc_deletes", "42"); try { client().admin().indices().prepareUpdateSettings("test").setSettings(newSettings).execute().actionGet(); fail("did not hit expected exception"); } catch (IllegalArgumentException iae) { // expected assertTrue(iae.getMessage().contains("failed to parse setting [index.gc_deletes] with value [42] as a time value: unit is missing or unrecognized")); } } public void testInternalVersioningInitialDelete() throws Exception { createIndex("test"); ensureGreen(); assertThrows(client().prepareDelete("test", "type", "1").setVersion(17).execute(), VersionConflictEngineException.class); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1") .setCreate(true).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(1L)); } public void testInternalVersioning() throws Exception { createIndex("test"); ensureGreen(); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(1L)); indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_2").setVersion(1).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(2L)); assertThrows( client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows( client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows( client().prepareIndex("test", "type", "1").setCreate(true).setSource("field1", "value1_1").execute(), VersionConflictEngineException.class); assertThrows(client().prepareDelete("test", "type", "1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows(client().prepareDelete("test", "type", "1").setVersion(1).execute(), VersionConflictEngineException.class); client().admin().indices().prepareRefresh().execute().actionGet(); for (int i = 0; i < 10; i++) { assertThat(client().prepareGet("test", "type", "1").execute().actionGet().getVersion(), equalTo(2L)); } // search with versioning for (int i = 0; i < 10; i++) { SearchResponse searchResponse = client().prepareSearch().setQuery(matchAllQuery()).setVersion(true).execute().actionGet(); assertThat(searchResponse.getHits().getAt(0).getVersion(), equalTo(2L)); } // search without versioning for (int i = 0; i < 10; i++) { SearchResponse searchResponse = client().prepareSearch().setQuery(matchAllQuery()).execute().actionGet(); assertThat(searchResponse.getHits().getAt(0).getVersion(), equalTo(Versions.NOT_FOUND)); } DeleteResponse deleteResponse = client().prepareDelete("test", "type", "1").setVersion(2).execute().actionGet(); assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(3L)); assertThrows(client().prepareDelete("test", "type", "1").setVersion(2).execute(), VersionConflictEngineException.class); // This is intricate - the object was deleted but a delete transaction was with the right version. We add another one // and thus the transaction is increased. deleteResponse = client().prepareDelete("test", "type", "1").setVersion(3).execute().actionGet(); assertEquals(DocWriteResponse.Result.NOT_FOUND, deleteResponse.getResult()); assertThat(deleteResponse.getVersion(), equalTo(4L)); } public void testSimpleVersioningWithFlush() throws Exception { createIndex("test"); ensureGreen(); IndexResponse indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(1L)); client().admin().indices().prepareFlush().execute().actionGet(); indexResponse = client().prepareIndex("test", "type", "1").setSource("field1", "value1_2").setVersion(1).execute().actionGet(); assertThat(indexResponse.getVersion(), equalTo(2L)); client().admin().indices().prepareFlush().execute().actionGet(); assertThrows(client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows(client().prepareIndex("test", "type", "1").setSource("field1", "value1_1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows(client().prepareIndex("test", "type", "1").setCreate(true).setSource("field1", "value1_1").execute(), VersionConflictEngineException.class); assertThrows(client().prepareDelete("test", "type", "1").setVersion(1).execute(), VersionConflictEngineException.class); assertThrows(client().prepareDelete("test", "type", "1").setVersion(1).execute(), VersionConflictEngineException.class); client().admin().indices().prepareRefresh().execute().actionGet(); for (int i = 0; i < 10; i++) { assertThat(client().prepareGet("test", "type", "1").execute().actionGet().getVersion(), equalTo(2L)); } for (int i = 0; i < 10; i++) { SearchResponse searchResponse = client().prepareSearch().setQuery(matchAllQuery()).setVersion(true).execute().actionGet(); assertThat(searchResponse.getHits().getAt(0).getVersion(), equalTo(2L)); } } public void testVersioningWithBulk() { createIndex("test"); ensureGreen(); BulkResponse bulkResponse = client().prepareBulk().add(client().prepareIndex("test", "type", "1").setSource("field1", "value1_1")).execute().actionGet(); assertThat(bulkResponse.hasFailures(), equalTo(false)); assertThat(bulkResponse.getItems().length, equalTo(1)); IndexResponse indexResponse = bulkResponse.getItems()[0].getResponse(); assertThat(indexResponse.getVersion(), equalTo(1L)); } // Poached from Lucene's TestIDVersionPostingsFormat: private interface IDSource { String next(); } private IDSource getRandomIDs() { IDSource ids; final Random random = random(); switch (random.nextInt(6)) { case 0: // random simple logger.info("--> use random simple ids"); ids = new IDSource() { @Override public String next() { return TestUtil.randomSimpleString(random, 1, 10); } }; break; case 1: // random realistic unicode logger.info("--> use random realistic unicode ids"); ids = new IDSource() { @Override public String next() { return TestUtil.randomRealisticUnicodeString(random, 1, 20); } }; break; case 2: // sequential logger.info("--> use sequential ids"); ids = new IDSource() { int upto; @Override public String next() { return Integer.toString(upto++); } }; break; case 3: // zero-pad sequential logger.info("--> use zero-padded sequential ids"); ids = new IDSource() { final int radix = TestUtil.nextInt(random, Character.MIN_RADIX, Character.MAX_RADIX); final String zeroPad = String.format(Locale.ROOT, "%0" + TestUtil.nextInt(random, 4, 20) + "d", 0); int upto; @Override public String next() { String s = Integer.toString(upto++); return zeroPad.substring(zeroPad.length() - s.length()) + s; } }; break; case 4: // random long logger.info("--> use random long ids"); ids = new IDSource() { final int radix = TestUtil.nextInt(random, Character.MIN_RADIX, Character.MAX_RADIX); int upto; @Override public String next() { return Long.toString(random.nextLong() & 0x3ffffffffffffffL, radix); } }; break; case 5: // zero-pad random long logger.info("--> use zero-padded random long ids"); ids = new IDSource() { final int radix = TestUtil.nextInt(random, Character.MIN_RADIX, Character.MAX_RADIX); final String zeroPad = String.format(Locale.ROOT, "%015d", 0); int upto; @Override public String next() { return Long.toString(random.nextLong() & 0x3ffffffffffffffL, radix); } }; break; default: throw new AssertionError(); } return ids; } private static class IDAndVersion { public String id; public long version; public boolean delete; public int threadID = -1; public long indexStartTime; public long indexFinishTime; public boolean versionConflict; public boolean alreadyExists; public ActionResponse response; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("id="); sb.append(id); sb.append(" version="); sb.append(version); sb.append(" delete?="); sb.append(delete); sb.append(" threadID="); sb.append(threadID); sb.append(" indexStartTime="); sb.append(indexStartTime); sb.append(" indexFinishTime="); sb.append(indexFinishTime); sb.append(" versionConflict="); sb.append(versionConflict); sb.append(" alreadyExists?="); sb.append(alreadyExists); if (response != null) { if (response instanceof DeleteResponse) { DeleteResponse deleteResponse = (DeleteResponse) response; sb.append(" response:"); sb.append(" index="); sb.append(deleteResponse.getIndex()); sb.append(" id="); sb.append(deleteResponse.getId()); sb.append(" type="); sb.append(deleteResponse.getType()); sb.append(" version="); sb.append(deleteResponse.getVersion()); sb.append(" found="); sb.append(deleteResponse.getResult() == DocWriteResponse.Result.DELETED); } else if (response instanceof IndexResponse) { IndexResponse indexResponse = (IndexResponse) response; sb.append(" index="); sb.append(indexResponse.getIndex()); sb.append(" id="); sb.append(indexResponse.getId()); sb.append(" type="); sb.append(indexResponse.getType()); sb.append(" version="); sb.append(indexResponse.getVersion()); sb.append(" created="); sb.append(indexResponse.getResult() == DocWriteResponse.Result.CREATED); } else { sb.append(" response: " + response); } } else { sb.append(" response: null"); } return sb.toString(); } } public void testRandomIDsAndVersions() throws Exception { createIndex("test"); ensureGreen(); // TODO: sometimes use _bulk API // TODO: test non-aborting exceptions (Rob suggested field where positions overflow) // TODO: not great we don't test deletes GC here: // We test deletes, but can't rely on wall-clock delete GC: HashMap<String, Object> newSettings = new HashMap<>(); newSettings.put("index.gc_deletes", "1000000h"); assertAcked(client().admin().indices().prepareUpdateSettings("test").setSettings(newSettings).execute().actionGet()); Random random = random(); // Generate random IDs: IDSource idSource = getRandomIDs(); Set<String> idsSet = new HashSet<>(); String idPrefix; if (randomBoolean()) { idPrefix = ""; } else { idPrefix = TestUtil.randomSimpleString(random); logger.debug("--> use id prefix {}", idPrefix); } int numIDs; if (TEST_NIGHTLY) { numIDs = scaledRandomIntBetween(300, 1000); } else { numIDs = scaledRandomIntBetween(50, 100); } while (idsSet.size() < numIDs) { idsSet.add(idPrefix + idSource.next()); } String[] ids = idsSet.toArray(new String[numIDs]); boolean useMonotonicVersion = randomBoolean(); // Attach random versions to them: long version = 0; final IDAndVersion[] idVersions = new IDAndVersion[TestUtil.nextInt(random, numIDs / 2, numIDs * (TEST_NIGHTLY ? 8 : 2))]; final Map<String, IDAndVersion> truth = new HashMap<>(); logger.debug("--> use {} ids; {} operations", numIDs, idVersions.length); for (int i = 0; i < idVersions.length; i++) { if (useMonotonicVersion) { version += TestUtil.nextInt(random, 1, 10); } else { version = random.nextLong() & 0x3fffffffffffffffL; } idVersions[i] = new IDAndVersion(); idVersions[i].id = ids[random.nextInt(numIDs)]; idVersions[i].version = version; // 20% of the time we delete: idVersions[i].delete = random.nextInt(5) == 2; IDAndVersion curVersion = truth.get(idVersions[i].id); if (curVersion == null || idVersions[i].version > curVersion.version) { // Save highest version per id: truth.put(idVersions[i].id, idVersions[i]); } } // Shuffle for (int i = idVersions.length - 1; i > 0; i--) { int index = random.nextInt(i + 1); IDAndVersion x = idVersions[index]; idVersions[index] = idVersions[i]; idVersions[i] = x; } for (IDAndVersion idVersion : idVersions) { logger.debug("--> id={} version={} delete?={} truth?={}", idVersion.id, idVersion.version, idVersion.delete, truth.get(idVersion.id) == idVersion); } final AtomicInteger upto = new AtomicInteger(); final CountDownLatch startingGun = new CountDownLatch(1); Thread[] threads = new Thread[TestUtil.nextInt(random, 1, TEST_NIGHTLY ? 20 : 5)]; final long startTime = System.nanoTime(); for (int i = 0; i < threads.length; i++) { final int threadID = i; threads[i] = new Thread() { @Override public void run() { try { //final Random threadRandom = RandomizedContext.current().getRandom(); final Random threadRandom = random(); startingGun.await(); while (true) { // TODO: sometimes use bulk: int index = upto.getAndIncrement(); if (index >= idVersions.length) { break; } if (index % 100 == 0) { logger.trace("{}: index={}", Thread.currentThread().getName(), index); } IDAndVersion idVersion = idVersions[index]; String id = idVersion.id; idVersion.threadID = threadID; idVersion.indexStartTime = System.nanoTime() - startTime; long version = idVersion.version; if (idVersion.delete) { try { idVersion.response = client().prepareDelete("test", "type", id) .setVersion(version) .setVersionType(VersionType.EXTERNAL).execute().actionGet(); } catch (VersionConflictEngineException vcee) { // OK: our version is too old assertThat(version, lessThanOrEqualTo(truth.get(id).version)); idVersion.versionConflict = true; } } else { try { idVersion.response = client().prepareIndex("test", "type", id) .setSource("foo", "bar") .setVersion(version).setVersionType(VersionType.EXTERNAL).get(); } catch (VersionConflictEngineException vcee) { // OK: our version is too old assertThat(version, lessThanOrEqualTo(truth.get(id).version)); idVersion.versionConflict = true; } } idVersion.indexFinishTime = System.nanoTime() - startTime; if (threadRandom.nextInt(100) == 7) { logger.trace("--> {}: TEST: now refresh at {}", threadID, System.nanoTime() - startTime); refresh(); logger.trace("--> {}: TEST: refresh done at {}", threadID, System.nanoTime() - startTime); } if (threadRandom.nextInt(100) == 7) { logger.trace("--> {}: TEST: now flush at {}", threadID, System.nanoTime() - startTime); flush(); logger.trace("--> {}: TEST: flush done at {}", threadID, System.nanoTime() - startTime); } } } catch (Exception e) { throw new RuntimeException(e); } } }; threads[i].start(); } startingGun.countDown(); for (Thread thread : threads) { thread.join(); } // Verify against truth: boolean failed = false; for (String id : ids) { long expected; IDAndVersion idVersion = truth.get(id); if (idVersion != null && idVersion.delete == false) { expected = idVersion.version; } else { expected = -1; } long actualVersion = client().prepareGet("test", "type", id).execute().actionGet().getVersion(); if (actualVersion != expected) { logger.error("--> FAILED: idVersion={} actualVersion= {}", idVersion, actualVersion); failed = true; } } if (failed) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < idVersions.length; i++) { sb.append("i=").append(i).append(" ").append(idVersions[i]).append(System.lineSeparator()); } logger.error("All versions: {}", sb); fail("wrong versions for some IDs"); } } public void testDeleteNotLost() throws Exception { // We require only one shard for this test, so that the 2nd delete provokes pruning the deletes map: client() .admin() .indices() .prepareCreate("test") .setSettings(Settings.builder() .put("index.number_of_shards", 1)) .execute(). actionGet(); ensureGreen(); HashMap<String, Object> newSettings = new HashMap<>(); newSettings.put("index.gc_deletes", "10ms"); newSettings.put("index.refresh_interval", "-1"); client() .admin() .indices() .prepareUpdateSettings("test") .setSettings(newSettings) .execute() .actionGet(); // Index a doc: client() .prepareIndex("test", "type", "id") .setSource("foo", "bar") .setOpType(DocWriteRequest.OpType.INDEX) .setVersion(10) .setVersionType(VersionType.EXTERNAL) .execute() .actionGet(); if (randomBoolean()) { // Force refresh so the add is sometimes visible in the searcher: refresh(); } // Delete it client() .prepareDelete("test", "type", "id") .setVersion(11) .setVersionType(VersionType.EXTERNAL) .execute() .actionGet(); // Real-time get should reflect delete: assertThat("doc should have been deleted", client() .prepareGet("test", "type", "id") .execute() .actionGet() .getVersion(), equalTo(-1L)); // ThreadPool.estimatedTimeInMillis has default granularity of 200 msec, so we must sleep at least that long; sleep much longer in // case system is busy: Thread.sleep(1000); // Delete an unrelated doc (provokes pruning deletes from versionMap) client() .prepareDelete("test", "type", "id2") .setVersion(11) .setVersionType(VersionType.EXTERNAL) .execute() .actionGet(); // Real-time get should still reflect delete: assertThat("doc should have been deleted", client() .prepareGet("test", "type", "id") .execute() .actionGet() .getVersion(), equalTo(-1L)); } public void testGCDeletesZero() throws Exception { createIndex("test"); ensureGreen(); // We test deletes, but can't rely on wall-clock delete GC: HashMap<String, Object> newSettings = new HashMap<>(); newSettings.put("index.gc_deletes", "0ms"); client() .admin() .indices() .prepareUpdateSettings("test") .setSettings(newSettings) .execute() .actionGet(); // Index a doc: client() .prepareIndex("test", "type", "id") .setSource("foo", "bar") .setOpType(DocWriteRequest.OpType.INDEX) .setVersion(10) .setVersionType(VersionType.EXTERNAL) .execute() .actionGet(); if (randomBoolean()) { // Force refresh so the add is sometimes visible in the searcher: refresh(); } // Delete it client() .prepareDelete("test", "type", "id") .setVersion(11) .setVersionType(VersionType.EXTERNAL) .execute() .actionGet(); // Real-time get should reflect delete even though index.gc_deletes is 0: assertThat("doc should have been deleted", client() .prepareGet("test", "type", "id") .execute() .actionGet() .getVersion(), equalTo(-1L)); } }