blob: bfe5454e41645c9786a4086038e7413f8b8fe4d0 [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.jackrabbit.oak.plugins.index.importer;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Properties;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.apache.felix.inventory.Format;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
import org.apache.jackrabbit.oak.plugins.index.inventory.IndexDefinitionPrinter;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexLookup;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.QueryEngineSettings;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfo;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.ast.SelectorImpl;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.Editor;
import org.apache.jackrabbit.oak.spi.commit.EditorHook;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.jetbrains.annotations.NotNull;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.collect.ImmutableSet.of;
import static java.util.Arrays.asList;
import static org.apache.jackrabbit.JcrConstants.NT_BASE;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ASYNC_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_COUNT;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_DISABLED;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexUtils.createIndexDefinition;
import static org.apache.jackrabbit.oak.plugins.index.importer.AsyncIndexerLock.NOOP_LOCK;
import static org.apache.jackrabbit.oak.plugins.index.importer.IndexDefinitionUpdater.INDEX_DEFINITIONS_JSON;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
public class IndexImporterTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder(new File("target"));
private NodeStore store = new MemoryNodeStore();
private IndexEditorProvider provider = new PropertyIndexEditorProvider();
@Test(expected = IllegalArgumentException.class)
public void importIndex_NoMeta() throws Exception{
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
}
@Test(expected = NullPointerException.class)
public void lostCheckpoint() throws Exception{
IndexerInfo info = new IndexerInfo(temporaryFolder.getRoot(), "unknown-checkpoint");
info.save();
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
}
@Test
public void switchLanes() throws Exception{
NodeBuilder builder = store.getRoot().builder();
builder.child("idx-a").setProperty("type", "property");
builder.child("idx-b").setProperty("type", "property");
builder.child("idx-b").setProperty("async", "async");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
createIndexDirs("/idx-a", "/idx-b");
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
importer.switchLanes();
NodeState idxa = NodeStateUtils.getNode(store.getRoot(), "/idx-a");
assertEquals(AsyncLaneSwitcher.getTempLaneName(IndexImporter.ASYNC_LANE_SYNC), idxa.getString(ASYNC_PROPERTY_NAME));
NodeState idxb = NodeStateUtils.getNode(store.getRoot(), "/idx-b");
assertEquals(AsyncLaneSwitcher.getTempLaneName("async"), idxb.getString(ASYNC_PROPERTY_NAME));
}
@Test(expected = NullPointerException.class)
public void importData_NoProvider() throws Exception{
NodeBuilder builder = store.getRoot().builder();
builder.child("idx-a").setProperty("type", "property");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
createIndexDirs("/idx-a");
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
importer.switchLanes();
importer.importIndexData();
}
@Test
public void importData_CallbackInvoked() throws Exception{
NodeBuilder builder = store.getRoot().builder();
builder.child("idx-a").setProperty("type", "property");
builder.child("idx-a").setProperty("foo", "bar");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
createIndexDirs("/idx-a");
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
IndexImporterProvider provider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir) {
assertEquals("bar", defn.getString("foo"));
assertEquals("idx-a", indexDir.getName());
defn.setProperty("imported", true);
}
@Override
public String getType() {
return "property";
}
};
importer.addImporterProvider(provider);
importer.switchLanes();
importer.importIndexData();
assertTrue(store.getRoot().getChildNode("idx-a").getBoolean("imported"));
}
@Test
public void importData_UpdatedIndexDefinition() throws Exception{
NodeBuilder builder = store.getRoot().builder();
builder.child("idx-a").setProperty("type", "property");
builder.child("idx-a").setProperty("foo", "bar");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
createIndexDirs("/idx-a");
//We remove foo property here
builder = store.getRoot().builder();
builder.child("idx-a").removeProperty("foo");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
IndexImporterProvider provider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir) {
//Foo property should be set by virtue of import from json
assertEquals("bar", defn.getString("foo"));
}
@Override
public String getType() {
return "property";
}
};
importer.addImporterProvider(provider);
importer.switchLanes();
importer.importIndexData();
}
@Test
public void importData_IncrementalUpdate() throws Exception{
NodeBuilder builder = store.getRoot().builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, false, ImmutableSet.of("foo"), null)
.setProperty(ASYNC_PROPERTY_NAME, "async");
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
String checkpoint = createIndexDirs("/oak:index/fooIndex");
builder = store.getRoot().builder();
builder.child("c").setProperty("foo", "abc");
builder.child("d").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
FilterImpl f = createFilter(store.getRoot(), NT_BASE);
PropertyIndexLookup lookup = new PropertyIndexLookup(store.getRoot());
assertEquals(of("a", "b", "c", "d"), find(lookup, "foo", "abc", f));
IndexImporterProvider importerProvider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir) {
assertEquals("fooIndex", indexDir.getName());
assertEquals(2, defn.getProperty(REINDEX_COUNT).getValue(Type.LONG).longValue());
defn.getChildNode(IndexConstants.INDEX_CONTENT_NODE_NAME).remove();
NodeState cpState = store.retrieve(checkpoint);
NodeState indexData = NodeStateUtils.getNode(cpState, "/oak:index/fooIndex/:index");
defn.setChildNode(IndexConstants.INDEX_CONTENT_NODE_NAME, indexData);
}
@Override
public String getType() {
return "property";
}
};
builder = store.getRoot().builder();
builder.child("e").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
importer.addImporterProvider(importerProvider);
importer.addImporterProvider(importerProvider);
importer.importIndex();
NodeState idx = store.getRoot().getChildNode("oak:index").getChildNode("fooIndex");
assertEquals("async", idx.getString("async"));
lookup = new PropertyIndexLookup(store.getRoot());
//It would not pickup /e as thats not yet indexed as part of last checkpoint
assertEquals(of("a", "b", "c", "d"), find(lookup, "foo", "abc", f));
assertNull(store.retrieve(checkpoint));
}
@Test
public void importIndex_newIndex() throws Exception{
NodeBuilder builder = store.getRoot().builder();
builder.child("oak:index");
builder.child("a").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
String json = "{\"/oak:index/fooIndex\": {\n" +
" \"reindexCount\": 1,\n" +
" \"reindex\": false,\n" +
" \"type\": \"property\",\n" +
" \"async\" : \"async\",\n" +
" \"propertyNames\": [\"foo\"],\n" +
" \"jcr:primaryType\": \"oak:QueryIndexDefinition\"\n" +
" }\n" +
"}";
File indexFolder = temporaryFolder.getRoot();
//Create checkpoint file
String checkpoint = store.checkpoint(1000000);
IndexerInfo info = new IndexerInfo(indexFolder, checkpoint);
info.save();
//Create index definitions json
Files.write(json, new File(indexFolder, INDEX_DEFINITIONS_JSON), UTF_8);
createIndexFolder(indexFolder, "/oak:index/fooIndex");
//Prepare importer
IndexImporterProvider importerProvider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir)
throws CommitFailedException {
NodeState fooIndex = getFooIndexNodeState();
defn.setChildNode(IndexConstants.INDEX_CONTENT_NODE_NAME,
fooIndex.getChildNode(":index"));
}
@Override
public String getType() {
return "property";
}
};
IndexImporter importer = new IndexImporter(store, indexFolder, provider, NOOP_LOCK);
importer.addImporterProvider(importerProvider);
//Add some more indexable data
builder = store.getRoot().builder();
builder.child("c").setProperty("foo", "abc");
builder.child("d").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
//Now perform import
importer.importIndex();
FilterImpl f = createFilter(store.getRoot(), NT_BASE);
PropertyIndexLookup lookup = new PropertyIndexLookup(store.getRoot());
assertEquals(of("a", "c", "d"), find(lookup, "foo", "abc", f));
NodeState idx = store.getRoot().getChildNode("oak:index").getChildNode("fooIndex");
assertEquals(2, idx.getLong("reindexCount"));
}
@Test
public void importData_DisabledIndexes() throws Exception{
NodeBuilder builder = store.getRoot().builder();
NodeBuilder idxa = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, false, ImmutableSet.of("foo"), null);
idxa.setProperty(ASYNC_PROPERTY_NAME, "async");
idxa.setProperty(IndexConstants.SUPERSEDED_INDEX_PATHS, asList("/oak:index/barIndex"), Type.STRINGS);
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
String checkpoint = createIndexDirs("/oak:index/fooIndex");
builder = store.getRoot().builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"barIndex", true, false, ImmutableSet.of("foo"), null);
builder.child("c").setProperty("foo", "abc");
builder.child("d").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
IndexImporterProvider importerProvider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir) {
}
@Override
public String getType() {
return "property";
}
};
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, NOOP_LOCK);
importer.addImporterProvider(importerProvider);
importer.importIndex();
NodeState idx = store.getRoot().getChildNode("oak:index").getChildNode("barIndex");
assertEquals(TYPE_DISABLED, idx.getString(TYPE_PROPERTY_NAME));
}
private NodeState getFooIndexNodeState() throws CommitFailedException {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "fooIndex",
true, false, ImmutableSet.of("foo"), null);
builder.child("a").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
EditorHook hook = new EditorHook(
new IndexUpdateProvider(new PropertyIndexEditorProvider()));
NodeState indexed = hook.processCommit(EMPTY_NODE, after, CommitInfo.EMPTY);
return indexed.getChildNode("oak:index").getChildNode("fooIndex");
}
@Test
public void laneName() throws Exception{
NodeBuilder builder = EMPTY_NODE.builder();
builder.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, "async");
assertEquals("async", IndexImporter.getAsyncLaneName("foo", builder.getNodeState()));
builder = EMPTY_NODE.builder();
builder.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, asList("async", "nrt"), Type.STRINGS);
assertEquals("async", IndexImporter.getAsyncLaneName("foo", builder.getNodeState()));
builder = EMPTY_NODE.builder();
builder.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, asList("async", "nrt"), Type.STRINGS);
AsyncLaneSwitcher.switchLane(builder, "tmp-async");
assertEquals("async", IndexImporter.getAsyncLaneName("foo", builder.getNodeState()));
builder = EMPTY_NODE.builder();
builder.setProperty(IndexConstants.ASYNC_PROPERTY_NAME, asList("async", "nrt"), Type.STRINGS);
AsyncLaneSwitcher.switchLane(builder, "tmp-async");
assertEquals("async", IndexImporter.getAsyncLaneName("foo", builder.getNodeState()));
}
@Test
public void laneUnlockedInCaseOfFailure() throws Exception{
NodeBuilder builder = store.getRoot().builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, false, ImmutableSet.of("foo"), null)
.setProperty(ASYNC_PROPERTY_NAME, "async");
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
String checkpoint = createIndexDirs("/oak:index/fooIndex");
builder = store.getRoot().builder();
builder.child("c").setProperty("foo", "abc");
builder.child("d").setProperty("foo", "abc");
store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
new AsyncIndexUpdate("async", store, provider).run();
IndexImporterProvider importerProvider = new IndexImporterProvider() {
@Override
public void importIndex(NodeState root, NodeBuilder defn, File indexDir) {
}
@Override
public String getType() {
return "property";
}
};
final String exceptionMessage = "TEST MESSAGE";
ClusterNodeStoreLock lock = new ClusterNodeStoreLock(store);
provider = new PropertyIndexEditorProvider() {
@Override
public Editor getIndexEditor(@NotNull String type, @NotNull NodeBuilder definition,
@NotNull NodeState root, @NotNull IndexUpdateCallback callback) {
throw new RuntimeException(exceptionMessage);
}
};
IndexImporter importer = new IndexImporter(store, temporaryFolder.getRoot(), provider, lock);
importer.addImporterProvider(importerProvider);
try {
importer.importIndex();
fail();
} catch (RuntimeException ignore) {
}
assertFalse(lock.isLocked("async"));
AsyncIndexerLock lock2 = new AsyncIndexerLock() {
@Override
public LockToken lock(String asyncIndexerLane) throws CommitFailedException {
return mock(LockToken.class);
}
@Override
public void unlock(LockToken token) throws CommitFailedException {
throw new IllegalStateException("Exception in unlock");
}
};
IndexImporter importer2 = new IndexImporter(store, temporaryFolder.getRoot(), provider, lock2);
importer2.addImporterProvider(importerProvider);
try {
importer2.importIndex();
fail();
} catch (RuntimeException ignore) {
assertEquals(exceptionMessage, ignore.getMessage());
}
}
private static FilterImpl createFilter(NodeState root, String nodeTypeName) {
NodeTypeInfoProvider nodeTypes = new NodeStateNodeTypeInfoProvider(root);
NodeTypeInfo type = nodeTypes.getNodeTypeInfo(nodeTypeName);
SelectorImpl selector = new SelectorImpl(type, nodeTypeName);
return new FilterImpl(selector, "SELECT * FROM [" + nodeTypeName + "]",
new QueryEngineSettings());
}
private static Set<String> find(PropertyIndexLookup lookup, String name,
String value, Filter filter) {
return Sets.newHashSet(lookup.query(filter, name, value == null ? null
: PropertyValues.newString(value)));
}
private String createIndexDirs(String... indexPaths) throws IOException, CommitFailedException {
String checkpoint = store.checkpoint(1000000);
IndexerInfo info = new IndexerInfo(temporaryFolder.getRoot(), checkpoint);
info.save();
for (String indexPath : indexPaths){
createIndexFolder(temporaryFolder.getRoot(), indexPath);
}
dumpIndexDefinitions(indexPaths);
return checkpoint;
}
private void createIndexFolder(File indexFolder, String indexPath) throws IOException {
String dirName = PathUtils.getName(indexPath);
File indexDir = new File(indexFolder, dirName);
File indexMeta = new File(indexDir, IndexerInfo.INDEX_METADATA_FILE_NAME);
Properties p = new Properties();
p.setProperty(IndexerInfo.PROP_INDEX_PATH, indexPath);
indexDir.mkdir();
PropUtils.writeTo(p, indexMeta, "index info");
}
private void dumpIndexDefinitions(String... indexPaths) throws IOException, CommitFailedException {
IndexDefinitionPrinter printer = new IndexDefinitionPrinter(store, () -> asList(indexPaths));
printer.setFilter("{\"properties\":[\"*\", \"-:childOrder\"],\"nodes\":[\"*\", \"-:index-definition\"]}");
File file = new File(temporaryFolder.getRoot(), INDEX_DEFINITIONS_JSON);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
printer.print(pw, Format.JSON, false);
Files.write(sw.toString(), file, UTF_8);
}
}