blob: fb4ec29a42d35baf4342c0adf4d086a0df7a2f0e [file] [log] [blame]
/*
* 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;
import java.util.List;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.handler.component.RealTimeGetComponent;
import org.apache.solr.request.SolrQueryRequest;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.apache.solr.common.params.CommonParams.VERSION_FIELD;
import static org.hamcrest.core.StringContains.containsString;
public class UpdateLogTest extends SolrTestCaseJ4 {
/** BytesRef that can be re-used to lookup doc with id "1" */
private static final BytesRef DOC_1_INDEXED_ID = new BytesRef("1");
static UpdateLog ulog = null;
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-tlog.xml", "schema-inplace-updates.xml");
try (SolrQueryRequest req = req()) {
UpdateHandler uhandler = req.getCore().getUpdateHandler();
((DirectUpdateHandler2) uhandler).getCommitTracker().setTimeUpperBound(100);
((DirectUpdateHandler2) uhandler).getCommitTracker().setOpenSearcher(false);
ulog = uhandler.getUpdateLog();
}
}
@AfterClass
public static void afterClass() {
ulog = null;
}
@Test
/**
* @see org.apache.solr.update.UpdateLog#applyPartialUpdates(BytesRef,long,long,SolrDocumentBase)
*/
public void testApplyPartialUpdatesOnMultipleInPlaceUpdatesInSequence() {
// Add a full update, two in-place updates and verify applying partial updates is working
ulogAdd(ulog, null, sdoc("id", "1", "title_s", "title1", "val1_i_dvo", "1", "_version_", "100"));
ulogAdd(ulog, 100L, sdoc("id", "1", "price", "1000", "val1_i_dvo", "2", "_version_", "101"));
ulogAdd(ulog, 101L, sdoc("id", "1", "val1_i_dvo", "3", "_version_", "102"));
Object partialUpdate = ulog.lookup(DOC_1_INDEXED_ID);
SolrDocument partialDoc = RealTimeGetComponent.toSolrDoc((SolrInputDocument)((List)partialUpdate).get(4),
h.getCore().getLatestSchema());
long prevVersion = (Long)((List)partialUpdate).get(3);
long prevPointer = (Long)((List)partialUpdate).get(2);
assertEquals(3L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertFalse(partialDoc.containsKey("title_s"));
long returnVal = ulog.applyPartialUpdates(DOC_1_INDEXED_ID, prevPointer, prevVersion, null, partialDoc);
assertEquals(0, returnVal);
assertEquals(1000, Integer.parseInt(partialDoc.getFieldValue("price").toString()));
assertEquals(3L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertEquals("title1", partialDoc.getFieldValue("title_s"));
// Add a full update, commit, then two in-place updates, and verify that applying partial updates is working (since
// the prevTlog and prevTlog2 are retained after a commit
ulogCommit(ulog);
if (random().nextBoolean()) { // sometimes also try a second commit
ulogCommit(ulog);
}
ulogAdd(ulog, 102L, sdoc("id", "1", "price", "2000", "val1_i_dvo", "4", "_version_", "200"));
ulogAdd(ulog, 200L, sdoc("id", "1", "val1_i_dvo", "5", "_version_", "201"));
partialUpdate = ulog.lookup(DOC_1_INDEXED_ID);
partialDoc = RealTimeGetComponent.toSolrDoc((SolrInputDocument)((List)partialUpdate).get(4), h.getCore().getLatestSchema());
prevVersion = (Long)((List)partialUpdate).get(3);
prevPointer = (Long)((List)partialUpdate).get(2);
assertEquals(5L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertFalse(partialDoc.containsKey("title_s"));
returnVal = ulog.applyPartialUpdates(DOC_1_INDEXED_ID, prevPointer, prevVersion, null, partialDoc);
assertEquals(0, returnVal);
assertEquals(2000, Integer.parseInt(partialDoc.getFieldValue("price").toString()));
assertEquals(5L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertEquals("title1", partialDoc.getFieldValue("title_s"));
}
@Test
public void testApplyPartialUpdatesAfterMultipleCommits() {
ulogAdd(ulog, null, sdoc("id", "1", "title_s", "title1", "val1_i_dvo", "1", "_version_", "100"));
ulogAdd(ulog, 100L, sdoc("id", "1", "price", "1000", "val1_i_dvo", "2", "_version_", "101"));
ulogAdd(ulog, 101L, sdoc("id", "1", "val1_i_dvo", "3", "_version_", "102"));
// Do 3 commits, then in-place update, and verify that applying partial updates can't find full doc
for (int i=0; i<3; i++)
ulogCommit(ulog);
ulogAdd(ulog, 101L, sdoc("id", "1", "val1_i_dvo", "6", "_version_", "300"));
Object partialUpdate = ulog.lookup(DOC_1_INDEXED_ID);
SolrDocument partialDoc = RealTimeGetComponent.toSolrDoc((SolrInputDocument)((List)partialUpdate).get(4), h.getCore().getLatestSchema());
long prevVersion = (Long)((List)partialUpdate).get(3);
long prevPointer = (Long)((List)partialUpdate).get(2);
assertEquals(6L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertFalse(partialDoc.containsKey("title_s"));
long returnVal = ulog.applyPartialUpdates(DOC_1_INDEXED_ID, prevPointer, prevVersion, null, partialDoc);
assertEquals(-1, returnVal);
}
@Test
public void testApplyPartialUpdatesDependingOnNonAddShouldThrowException() {
ulogAdd(ulog, null, sdoc("id", "1", "title_s", "title1", "val1_i_dvo", "1", "_version_", "100"));
ulogDelete(ulog, "1", 500L, false); // dbi
ulogAdd(ulog, 500L, sdoc("id", "1", "val1_i_dvo", "2", "_version_", "501"));
ulogAdd(ulog, 501L, sdoc("id", "1", "val1_i_dvo", "3", "_version_", "502"));
Object partialUpdate = ulog.lookup(DOC_1_INDEXED_ID);
SolrDocument partialDoc = RealTimeGetComponent.toSolrDoc((SolrInputDocument)((List)partialUpdate).get(4), h.getCore().getLatestSchema());
long prevVersion = (Long)((List)partialUpdate).get(3);
long prevPointer = (Long)((List)partialUpdate).get(2);
assertEquals(3L, ((NumericDocValuesField)partialDoc.getFieldValue("val1_i_dvo")).numericValue());
assertEquals(502L, ((NumericDocValuesField)partialDoc.getFieldValue("_version_")).numericValue());
assertFalse(partialDoc.containsKey("title_s"));
// If an in-place update depends on a non-add (i.e. DBI), assert that an exception is thrown.
SolrException ex = expectThrows(SolrException.class, () -> {
long returnVal = ulog.applyPartialUpdates(DOC_1_INDEXED_ID, prevPointer, prevVersion, null, partialDoc);
fail("502 depends on 501, 501 depends on 500, but 500 is a"
+ " DELETE. This should've generated an exception. returnVal is: "+returnVal);
});
assertEquals(ex.toString(), SolrException.ErrorCode.INVALID_STATE.code, ex.code());
assertThat(ex.getMessage(), containsString("should've been either ADD or UPDATE_INPLACE"));
assertThat(ex.getMessage(), containsString("looking for id=1"));
}
@Test
public void testApplyPartialUpdatesWithDelete() throws Exception {
ulogAdd(ulog, null, sdoc("id", "1", "title_s", "title1", "val1_i_dvo", "1", "_version_", "100"));
ulogAdd(ulog, 100L, sdoc("id", "1", "val1_i_dvo", "2", "_version_", "101")); // in-place update
ulogAdd(ulog, 101L, sdoc("id", "1", "val1_i_dvo", "3", "_version_", "102")); // in-place update
// sanity check that the update log has one document, and RTG returns the document
assertEquals(1, ulog.map.size());
assertJQ(req("qt","/get", "id","1")
, "=={'doc':{ 'id':'1', 'val1_i_dvo':3, '_version_':102, 'title_s':'title1', "
// fields with default values
+ "'inplace_updatable_int_with_default':666, 'inplace_updatable_float_with_default':42.0}}");
boolean dbq = random().nextBoolean();
ulogDelete(ulog, "1", 200L, dbq); // delete id:1 document
if (dbq) {
assertNull(ulog.lookup(DOC_1_INDEXED_ID)); // any DBQ clears out the ulog, so this document shouldn't exist
assertEquals(0, ulog.map.size());
assertTrue(String.valueOf(ulog.prevMap), ulog.prevMap == null || ulog.prevMap.size() == 0);
assertTrue(String.valueOf(ulog.prevMap2), ulog.prevMap2 == null || ulog.prevMap2.size() == 0);
// verify that the document is deleted, by doing an RTG call
assertJQ(req("qt","/get", "id","1"), "=={'doc':null}");
} else { // dbi
@SuppressWarnings({"rawtypes"})
List entry = ((List)ulog.lookup(DOC_1_INDEXED_ID));
assertEquals(UpdateLog.DELETE, (int)entry.get(UpdateLog.FLAGS_IDX) & UpdateLog.OPERATION_MASK);
}
}
/**
* Simulate a commit on a given updateLog
*/
private static void ulogCommit(UpdateLog ulog) {
try (SolrQueryRequest req = req()) {
CommitUpdateCommand commitCmd = new CommitUpdateCommand(req, false);
ulog.preCommit(commitCmd);
ulog.postCommit(commitCmd);
}
}
/**
* Simulate a delete on a given updateLog
*
* @param ulog The UpdateLog to apply a delete against
* @param id of document to be deleted
* @param version Version to use on the DeleteUpdateCommand
* @param dbq if true, an <code>id:$id</code> DBQ will used, instead of delete by id
*/
private static void ulogDelete(UpdateLog ulog, String id, long version, boolean dbq) {
try (SolrQueryRequest req = req()) {
DeleteUpdateCommand cmd = new DeleteUpdateCommand(req);
cmd.setVersion(version);
if (dbq) {
cmd.query = ("id:"+id);
ulog.deleteByQuery(cmd);
} else {
cmd.id = id;
ulog.delete(cmd);
}
}
}
/**
* Simulate an add on a given updateLog.
* <p>
* This method, when prevVersion is passed in (i.e. for in-place update), represents an
* AddUpdateCommand that has undergone the merge process and inc/set operations have now been
* converted into actual values that just need to be written.
* </p>
* <p>
* NOTE: For test simplicity, the Solr input document must include the <code>_version_</code> field.
* </p>
*
* @param ulog The UpdateLog to apply a delete against
* @param prevVersion If non-null, then this AddUpdateCommand represents an in-place update.
* @param sdoc The document to use for the add.
* @see #buildAddUpdateCommand
*/
private static void ulogAdd(UpdateLog ulog, Long prevVersion, SolrInputDocument sdoc) {
try (SolrQueryRequest req = req()) {
AddUpdateCommand cmd = buildAddUpdateCommand(req, sdoc);
if (prevVersion != null) {
cmd.prevVersion = prevVersion;
}
ulog.add(cmd);
}
}
/**
* Helper method to construct an <code>AddUpdateCommand</code> for a <code>SolrInputDocument</code>
* in the context of the specified <code>SolrQueryRequest</code>.
*
* NOTE: For test simplicity, the Solr input document must include the <code>_version_</code> field.
*/
public static AddUpdateCommand buildAddUpdateCommand(final SolrQueryRequest req, final SolrInputDocument sdoc) {
AddUpdateCommand cmd = new AddUpdateCommand(req);
cmd.solrDoc = sdoc;
assertTrue("", cmd.solrDoc.containsKey(VERSION_FIELD));
cmd.setVersion(Long.parseLong(cmd.solrDoc.getFieldValue(VERSION_FIELD).toString()));
return cmd;
}
}