/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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.apache.solr.update.processor; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.lucene.util.TestUtil; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.util.ExecutorUtil; import org.apache.solr.common.util.NamedList; import org.apache.solr.schema.IndexSchema; import org.apache.solr.schema.SchemaField; import org.apache.solr.common.util.SolrNamedThreadFactory; import org.junit.Before; import org.junit.BeforeClass; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; public class TestDocBasedVersionConstraints extends SolrTestCaseJ4 { @BeforeClass public static void beforeClass() throws Exception { initCore("solrconfig-externalversionconstraint.xml", "schema15.xml"); } @Before public void before() throws Exception { assertU(delQ("*:*")); assertU(commit()); } public void testSimpleUpdates() throws Exception { // skip low version against committed data assertU(adoc("id", "aaa", "name", "a1", "my_version_l", "1001")); assertU(commit()); assertU(adoc("id", "aaa", "name", "a2", "my_version_l", "1002")); assertU(commit()); assertU(adoc("id", "aaa", "name", "XX", "my_version_l", "1")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("q","+id:aaa +name:a2"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); // skip low version against uncommitted data from updateLog assertU(adoc("id", "aaa", "name", "a3", "my_version_l", "1003")); assertU(adoc("id", "aaa", "name", "XX", "my_version_l", "7")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a3'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("q","+id:aaa +name:a3"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a3'}}"); // interleave updates to multiple docs using same versions for (long ver = 1010; ver < 1020; ver++) { for (String id : new String[] {"aaa", "bbb", "ccc", "ddd"}) { assertU(adoc("id", id, "my_version_l", ""+ver)); } } for (String id : new String[] {"aaa", "bbb", "ccc", "ddd"}) { assertU(adoc("id", id, "name", "XX", "my_version_l", "10")); assertJQ(req("qt","/get", "id",id, "fl","my_version_l") , "=={'doc':{'my_version_l':"+1019+"}}"); } assertU(commit()); assertJQ(req("q","name:XX"), "/response/numFound==0"); for (String id : new String[] {"aaa", "bbb", "ccc", "ddd"}) { assertJQ(req("q","+id:"+id), "/response/numFound==1"); assertJQ(req("q","+name:XX +id:"+id), "/response/numFound==0"); assertJQ(req("q","+id:"+id + " +my_version_l:1019"), "/response/numFound==1"); assertJQ(req("qt","/get", "id",id, "fl","my_version_l") , "=={'doc':{'my_version_l':"+1019+"}}"); } } public void testSimpleDeletes() throws Exception { // skip low version delete against committed doc assertU(adoc("id", "aaa", "name", "a1", "my_version_l", "1001")); assertU(commit()); assertU(adoc("id", "aaa", "name", "a2", "my_version_l", "1002")); assertU(commit()); deleteAndGetVersion("aaa", params("del_version", "7")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:a2"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); // skip low version delete against uncommitted doc from updateLog assertU(adoc("id", "aaa", "name", "a3", "my_version_l", "1003")); deleteAndGetVersion("aaa", params("del_version", "8")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a3'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:a3"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a3'}}"); // skip low version add against uncommitted "delete" from updateLog deleteAndGetVersion("aaa", params("del_version", "1010")); assertU(adoc("id", "aaa", "name", "XX", "my_version_l", "22")); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}"); // skip low version add against committed "delete" // (delete was already done & committed above) assertU(adoc("id", "aaa", "name", "XX", "my_version_l", "23")); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}"); } /** * Sanity check that there are no hardcoded assumptions about the * field type used that could byte us in the ass. */ public void testFloatVersionField() throws Exception { // skip low version add & low version delete against committed doc updateJ(jsonAdd(sdoc("id", "aaa", "name", "a1", "my_version_f", "10.01")), params("update.chain","external-version-float")); assertU(commit()); updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_f", "4.2")), params("update.chain","external-version-float")); assertU(commit()); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a1'}}"); deleteAndGetVersion("aaa", params("del_version", "7", "update.chain","external-version-float")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a1'}}"); assertU(commit()); // skip low version delete against uncommitted doc from updateLog updateJ(jsonAdd(sdoc("id", "aaa", "name", "a2", "my_version_f", "10.02")), params("update.chain","external-version-float")); deleteAndGetVersion("aaa", params("del_version", "8", "update.chain","external-version-float")); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:a2"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); // skip low version add against uncommitted "delete" from updateLog deleteAndGetVersion("aaa", params("del_version", "10.10", "update.chain","external-version-float")); updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_f", "10.05")), params("update.chain","external-version-float")); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_f") , "=={'doc':{'my_version_f':10.10}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_f") , "=={'doc':{'my_version_f':10.10}}"); // skip low version add against committed "delete" // (delete was already done & committed above) updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_f", "10.09")), params("update.chain","external-version-float")); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_f") , "=={'doc':{'my_version_f':10.10}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_f") , "=={'doc':{'my_version_f':10.10}}"); } public void testFailOnOldVersion() throws Exception { // fail low version add & low version delete against committed doc updateJ(jsonAdd(sdoc("id", "aaa", "name", "a1", "my_version_l", "1001")), params("update.chain","external-version-failhard")); assertU(commit()); SolrException ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_l", "42")), params("update.chain","external-version-failhard")); }); assertEquals(409, ex.code()); assertU(commit()); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a1'}}"); ex = expectThrows(SolrException.class, () -> { deleteAndGetVersion("aaa", params("del_version", "7", "update.chain","external-version-failhard")); }); assertEquals(409, ex.code()); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a1'}}"); assertU(commit()); // fail low version delete against uncommitted doc from updateLog updateJ(jsonAdd(sdoc("id", "aaa", "name", "a2", "my_version_l", "1002")), params("update.chain","external-version-failhard")); ex = expectThrows(SolrException.class, () -> { deleteAndGetVersion("aaa", params("del_version", "8", "update.chain","external-version-failhard")); }); assertEquals(409, ex.code()); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:a2"), "/response/numFound==1"); assertJQ(req("qt","/get", "id","aaa", "fl","name") , "=={'doc':{'name':'a2'}}"); // fail low version add against uncommitted "delete" from updateLog deleteAndGetVersion("aaa", params("del_version", "1010", "update.chain","external-version-failhard")); ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_l", "1005")), params("update.chain","external-version-failhard")); }); assertEquals(409, ex.code()); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}"); // fail low version add against committed "delete" // (delete was already done & committed above) ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "XX", "my_version_l", "1009")), params("update.chain","external-version-failhard")); }); assertEquals(409, ex.code()); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}}"); assertU(commit()); assertJQ(req("q","+id:aaa"), "/response/numFound==1"); assertJQ(req("q","+id:aaa +name:XX"), "/response/numFound==0"); assertJQ(req("qt","/get", "id","aaa", "fl","my_version_l") , "=={'doc':{'my_version_l':1010}}"); } // Test multiple versions, that it has to be greater than my_version_l and my_version_f public void testMultipleVersions() throws Exception { updateJ(jsonAdd(sdoc("id", "aaa", "name", "a1", "my_version_l", "1001", "my_version_f", "1.0")), params("update.chain","external-version-failhard-multiple")); assertU(commit()); // All variations of additional versions should fail other than my_version_l greater or my_version_f greater. SolrException ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "X1", "my_version_l", "1000", "my_version_f", "1.0")), params("update.chain","external-version-failhard-multiple")); }); assertEquals(409, ex.code()); ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "X2", "my_version_l", "1001", "my_version_f", "0.9")), params("update.chain","external-version-failhard-multiple")); }); assertEquals(409, ex.code()); // Also fails on the exact same version ex = expectThrows(SolrException.class, () -> { updateJ(jsonAdd(sdoc("id", "aaa", "name", "X3", "my_version_l", "1001", "my_version_f", "1.0")), params("update.chain","external-version-failhard-multiple")); }); assertEquals(409, ex.code()); //Verify we are still unchanged assertU(commit()); assertJQ(req("q","+id:aaa +name:a1"), "/response/numFound==1"); // update version 1 updateJ(jsonAdd(sdoc("id", "aaa", "name", "Y1", "my_version_l", "2001", "my_version_f", "1.0")), params("update.chain","external-version-failhard-multiple")); assertU(commit()); assertJQ(req("q","+id:aaa +name:Y1"), "/response/numFound==1"); // update version 2 updateJ(jsonAdd(sdoc("id", "aaa", "name", "Y2", "my_version_l", "2001", "my_version_f", "2.0")), params("update.chain","external-version-failhard-multiple")); assertU(commit()); assertJQ(req("q","+id:aaa +name:Y2"), "/response/numFound==1"); } public void testMultipleVersionDeletes() throws Exception { updateJ(jsonAdd(sdoc("id", "aaa", "name", "a1", "my_version_l", "1001", "my_version_f", "1.0")), params("update.chain","external-version-failhard-multiple")); assertU(commit()); SolrException ex = expectThrows(SolrException.class, () -> { deleteAndGetVersion("aaa", params("del_version", "1000", "del_version_2", "1.0", "update.chain","external-version-failhard-multiple")); }); assertEquals(409, ex.code()); ex = expectThrows(SolrException.class, () -> { deleteAndGetVersion("aaa", params("del_version", "1001", "del_version_2", "0.9", "update.chain","external-version-failhard-multiple")); }); assertEquals(409, ex.code()); // And just verify if we pass version 1, we still error if version 2 isn't found. ignoreException("Delete by ID must specify doc version param"); ex = expectThrows(SolrException.class, () -> { deleteAndGetVersion("aaa", params("del_version", "1001", "update.chain","external-version-failhard-multiple")); }); assertEquals(400, ex.code()); unIgnoreException("Delete by ID must specify doc version param"); //Verify we are still unchanged assertU(commit()); assertJQ(req("q","+id:aaa +name:a1"), "/response/numFound==1"); //And let's verify the actual case. deleteAndGetVersion("aaa", params("del_version", "1001", "del_version_2", "2.0", "update.chain","external-version-failhard-multiple")); assertU(commit()); assertJQ(req("q","+id:aaa +name:a1"), "/response/numFound==0"); //Delete allowed } /** * Proof of concept test demonstrating how to manage and periodically cleanup * the "logically" deleted documents */ public void testManagingDeletes() throws Exception { // add some docs for (long ver = 1010; ver < 1020; ver++) { for (String id : new String[] {"aaa", "bbb", "ccc", "ddd"}) { assertU(adoc("id", id, "name", "name_"+id, "my_version_l", ""+ver)); } } assertU(adoc("id", "aaa", "name", "name_aaa", "my_version_l", "1030")); assertU(commit()); // sample queries assertJQ(req("q","*:*", "fq","live_b:true") ,"/response/numFound==4"); assertJQ(req("q","id:aaa", "fq","live_b:true", "fl","id,my_version_l") ,"/response/numFound==1" ,"/response/docs==[{'id':'aaa','my_version_l':1030}]}"); // logically delete deleteAndGetVersion("aaa", params("del_version", "1031")); assertU(commit()); // sample queries assertJQ(req("q","*:*", "fq","live_b:true") ,"/response/numFound==3"); assertJQ(req("q","id:aaa", "fq","live_b:true") ,"/response/numFound==0"); // placeholder doc is still in the index though assertJQ(req("q","id:aaa", "fq","live_b:false", "fq", "timestamp_tdt:[* TO *]", "fl","id,live_b,my_version_l") ,"/response/numFound==1" ,"/response/docs==[{'id':'aaa','my_version_l':1031,'live_b':false}]}"); // doc can't be re-added with a low version assertU(adoc("id", "aaa", "name", "XX", "my_version_l", "1025")); assertU(commit()); assertJQ(req("q","id:aaa", "fq","live_b:true") ,"/response/numFound==0"); // "dead" placeholder docs can be periodically cleaned up // ie: assertU(delQ("+live_b:false +timestamp_tdt:[* TO NOW/MINUTE-5MINUTE]")); // but to prevent the test from ebing time sensitive we'll just purge them all assertU(delQ("+live_b:false")); assertU(commit()); // now doc can be re-added w/any version, no matter how low assertU(adoc("id", "aaa", "name", "aaa", "my_version_l", "7")); assertU(commit()); assertJQ(req("q","id:aaa", "fq","live_b:true", "fl","id,live_b,my_version_l") ,"/response/numFound==1" ,"/response/docs==[{'id':'aaa','my_version_l':7,'live_b':true}]}"); } /** * Constantly hammer the same doc with multiple concurrent threads and diff versions, * confirm that the highest version wins. */ public void testConcurrentAdds() throws Exception { final int NUM_DOCS = atLeast(50); final int MAX_CONCURENT = atLeast(10); ExecutorService runner = ExecutorUtil.newMDCAwareFixedThreadPool(MAX_CONCURENT, new SolrNamedThreadFactory("TestDocBasedVersionConstraints")); // runner = Executors.newFixedThreadPool(1); // to test single threaded try { for (int id = 0; id < NUM_DOCS; id++) { final int numAdds = TestUtil.nextInt(random(), 3, MAX_CONCURENT); final int winner = TestUtil.nextInt(random(), 0, numAdds - 1); final int winnerVersion = atLeast(100); final boolean winnerIsDeleted = (0 == TestUtil.nextInt(random(), 0, 4)); List<Callable<Object>> tasks = new ArrayList<>(numAdds); for (int variant = 0; variant < numAdds; variant++) { final boolean iShouldWin = (variant==winner); final long version = (iShouldWin ? winnerVersion : TestUtil.nextInt(random(), 1, winnerVersion - 1)); if ((iShouldWin && winnerIsDeleted) || (!iShouldWin && 0 == TestUtil.nextInt(random(), 0, 4))) { tasks.add(delayedDelete(""+id, ""+version)); } else { tasks.add(delayedAdd("id",""+id,"name","name"+id+"_"+variant, "my_version_l", ""+ version)); } } runner.invokeAll(tasks); final String expectedDoc = "{'id':'"+id+"','my_version_l':"+winnerVersion + ( ! winnerIsDeleted ? ",'name':'name"+id+"_"+winner+"'}" : "}"); assertJQ(req("qt","/get", "id",""+id, "fl","id,name,my_version_l") , "=={'doc':" + expectedDoc + "}"); assertU(commit()); assertJQ(req("q","id:"+id, "fl","id,name,my_version_l") ,"/response/numFound==1" ,"/response/docs==["+expectedDoc+"]"); } } finally { ExecutorUtil.shutdownAndAwaitTermination(runner); } } public void testMissingVersionOnOldDocs() throws Exception { String version = "2"; // Write one doc with version, one doc without version using the "no version" chain updateJ(json("[{\"id\": \"a\", \"name\": \"a1\", \"my_version_l\": " + version + "}]"), params("update.chain", "no-external-version")); updateJ(json("[{\"id\": \"b\", \"name\": \"b1\"}]"), params("update.chain", "no-external-version")); assertU(commit()); assertJQ(req("q","*:*"), "/response/numFound==2"); assertJQ(req("q","id:a"), "/response/numFound==1"); assertJQ(req("q","id:b"), "/response/numFound==1"); // Try updating both with a new version and using the enforced version chain, expect id=b to fail bc old // doc is missing the version field String newVersion = "3"; updateJ(json("[{\"id\": \"a\", \"name\": \"a1\", \"my_version_l\": " + newVersion + "}]"), params("update.chain", "external-version-constraint")); ignoreException("Doc exists in index, but has null versionField: my_version_l"); SolrException ex = expectThrows(SolrException.class, () -> { updateJ(json("[{\"id\": \"b\", \"name\": \"b1\", \"my_version_l\": " + newVersion + "}]"), params("update.chain", "external-version-constraint")); }); assertEquals("Doc exists in index, but has null versionField: my_version_l", ex.getMessage()); unIgnoreException("Doc exists in index, but has null versionField: my_version_l"); assertU(commit()); assertJQ(req("q","*:*"), "/response/numFound==2"); assertJQ(req("qt","/get", "id", "a", "fl", "id,my_version_l"), "=={'doc':{'id':'a', 'my_version_l':3}}"); // version changed to 3 assertJQ(req("qt","/get", "id", "b", "fl", "id,my_version_l"), "=={'doc':{'id':'b'}}"); // no version, because update failed // Try to update again using the external version enforcement, but allowing old docs to not have the version // field. Expect id=a to fail because version is lower, expect id=b to succeed. version = "1"; updateJ(json("[{\"id\": \"a\", \"name\": \"a1\", \"my_version_l\": " + version + "}]"), params("update.chain", "external-version-support-missing")); System.out.println("send b"); updateJ(json("[{\"id\": \"b\", \"name\": \"b1\", \"my_version_l\": " + version + "}]"), params("update.chain", "external-version-support-missing")); assertU(commit()); assertJQ(req("q","*:*"), "/response/numFound==2"); assertJQ(req("qt","/get", "id", "a", "fl", "id,my_version_l"), "=={'doc':{'id':'a', 'my_version_l':3}}"); assertJQ(req("qt","/get", "id", "b", "fl", "id,my_version_l"), "=={'doc':{'id':'b', 'my_version_l':1}}"); } public void testTombstoneConfig() throws Exception { assertJQ(req("q","*:*"),"/response/numFound==0"); updateWithChain("tombstone-config", "id", "b!doc1", "my_version_l", "1"); assertU(commit()); assertJQ(req("q","*:*"),"/response/numFound==1"); assertJQ(req("q","foo_b:true"),"/response/numFound==0"); assertJQ(req("q","foo_i:1"),"/response/numFound==0"); assertJQ(req("q","foo_l:1"),"/response/numFound==0"); assertJQ(req("q","foo_f:1.5"),"/response/numFound==0"); assertJQ(req("q","foo_s:bar"),"/response/numFound==0"); assertJQ(req("q","foo_ss:bar1"),"/response/numFound==0"); assertJQ(req("q","foo_ss:bar2"),"/response/numFound==0"); deleteAndGetVersion("b!doc1", params("del_version", "2", "update.chain", "tombstone-config")); assertU(commit()); assertJQ(req("q","foo_b:true"),"/response/numFound==1"); assertJQ(req("q","foo_i:1"),"/response/numFound==1"); assertJQ(req("q","foo_l:1"),"/response/numFound==1"); assertJQ(req("q","foo_f:1.5"),"/response/numFound==1"); assertJQ(req("q","foo_s:bar"),"/response/numFound==1"); assertJQ(req("q","foo_ss:bar1"),"/response/numFound==1"); assertJQ(req("q","foo_ss:bar2"),"/response/numFound==1"); } public void testCanCreateTombstonesBasic() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); assertThat(factory.canCreateTombstoneDocument(schema), is(true)); } public void testCanCreateTombstonesMissingRequiredField() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); SchemaField sf = schema.getField("sku1"); assertThat(sf, is(not(nullValue()))); assertThat(schema.getRequiredFields(), not(hasItem(sf))); try { schema.getRequiredFields().add(sf); assertThat(factory.canCreateTombstoneDocument(schema), is(false)); } finally { schema.getRequiredFields().remove(sf); } } public void testCanCreateTombstonesRequiredFieldWithDefault() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); SchemaField sf = schema.getField("sku1"); SchemaField sf2 = new SchemaField("sku1_with_default", sf.getType(), sf.getProperties(), "foo"); try { schema.getRequiredFields().add(sf2); assertThat(factory.canCreateTombstoneDocument(schema), is(true)); } finally { schema.getRequiredFields().remove(sf2); } } public void testCanCreateTombstonesRequiredFieldInTombstoneConfig() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); NamedList<Object> tombstoneConfig = new NamedList<>(); config.add("tombstoneConfig", tombstoneConfig); tombstoneConfig.add("sku1", "foo"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); SchemaField sf = schema.getField("sku1"); assertThat(sf, is(not(nullValue()))); assertThat(schema.getRequiredFields(), not(hasItem(sf))); try { schema.getRequiredFields().add(sf); assertThat(factory.canCreateTombstoneDocument(schema), is(true)); } finally { schema.getRequiredFields().remove(sf); } } public void testCanCreateTombstonesVersionFieldRequired() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); SchemaField versionField = schema.getField("_version_"); assertThat(versionField, is(not(nullValue()))); assertThat(schema.getRequiredFields(), not(hasItem(versionField))); try { schema.getRequiredFields().add(versionField); assertThat(factory.canCreateTombstoneDocument(schema), is(true)); } finally { schema.getRequiredFields().remove(versionField); } } public void testCanCreateTombstonesUniqueKeyFieldRequired() { DocBasedVersionConstraintsProcessorFactory factory = new DocBasedVersionConstraintsProcessorFactory(); NamedList<Object> config = new NamedList<>(); config.add("versionField", "_version_"); factory.init(config); IndexSchema schema = h.getCore().getLatestSchema(); SchemaField uniqueKeyField = schema.getField("id"); assertThat(uniqueKeyField, is(not(nullValue()))); assertThat(uniqueKeyField, equalTo(schema.getUniqueKeyField())); assertThat(schema.getRequiredFields(), hasItem(schema.getUniqueKeyField())); assertThat(factory.canCreateTombstoneDocument(schema), is(true)); } private void updateWithChain(String chain, String...fields) throws Exception { assert fields.length % 2 == 0; SolrInputDocument doc = new SolrInputDocument(fields); updateJ(jsonAdd(doc), params("update.chain", chain)); } private Callable<Object> delayedAdd(final String... fields) { return Executors.callable(() -> { // log.info("ADDING DOC: " + adoc(fields)); assertU(adoc(fields)); }); } private Callable<Object> delayedDelete(final String id, final String externalVersion) { return Executors.callable(() -> { try { // Why does this throw "Exception" ??? // log.info("DELETING DOC: " + id + " v="+externalVersion); deleteAndGetVersion(id, params("del_version", externalVersion)); } catch (Exception e) { throw new RuntimeException(e); } }); } }