blob: f172c577e876cf41442f43146c7e9b3b523c94fe [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.lucene;
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
import org.apache.jackrabbit.oak.api.jmx.CheckpointMBean;
import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
import org.apache.jackrabbit.oak.plugins.blob.datastore.CachingFileDataStore;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreUtils;
import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyService;
import org.apache.jackrabbit.oak.plugins.index.AsyncIndexInfoService;
import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.IndexPathService;
import org.apache.jackrabbit.oak.plugins.index.fulltext.PreExtractedTextProvider;
import org.apache.jackrabbit.oak.plugins.index.importer.IndexImporterProvider;
import org.apache.jackrabbit.oak.plugins.index.lucene.CopyOnReadStatsMBean;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexAugmentorFactory;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexCopier;
import org.apache.jackrabbit.oak.plugins.index.lucene.IndexTracker;
import org.apache.jackrabbit.oak.plugins.index.lucene.LoggingInfoStream;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProvider;
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProviderService;
import org.apache.jackrabbit.oak.plugins.index.lucene.directory.BufferedOakDirectory;
import org.apache.jackrabbit.oak.plugins.index.lucene.property.PropertyIndexCleaner;
import org.apache.jackrabbit.oak.plugins.index.lucene.reader.DefaultIndexReaderFactory;
import org.apache.jackrabbit.oak.plugins.index.lucene.score.ScorerProviderFactory;
import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache;
import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore;
import org.apache.jackrabbit.oak.spi.commit.BackgroundObserver;
import org.apache.jackrabbit.oak.spi.commit.Observer;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
import org.apache.jackrabbit.oak.stats.StatisticsProvider;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.util.InfoStream;
import org.apache.sling.testing.mock.osgi.MockOsgi;
import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.osgi.framework.ServiceReference;
import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.TYPE_LUCENE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class LuceneIndexProviderServiceTest {
/*
The test case uses raw config name and not access it via
constants in LuceneIndexProviderService to ensure that change
in names are detected
*/
@Rule
public final TemporaryFolder folder = new TemporaryFolder(new File("target"));
@Rule
public final OsgiContext context = new OsgiContext();
private LuceneIndexProviderService service = new LuceneIndexProviderService();
private Whiteboard wb;
private MountInfoProvider mip;
@Before
public void setUp(){
mip = Mounts.newBuilder().build();
context.registerService(MountInfoProvider.class, mip);
context.registerService(StatisticsProvider.class, StatisticsProvider.NOOP);
context.registerService(ScorerProviderFactory.class, ScorerProviderFactory.DEFAULT);
context.registerService(IndexAugmentorFactory.class, new IndexAugmentorFactory());
context.registerService(NodeStore.class, new MemoryNodeStore());
context.registerService(IndexPathService.class, mock(IndexPathService.class));
context.registerService(AsyncIndexInfoService.class, mock(AsyncIndexInfoService.class));
context.registerService(CheckpointMBean.class, mock(CheckpointMBean.class));
wb = new OsgiWhiteboard(context.bundleContext());
MockOsgi.injectServices(service, context.bundleContext());
}
@After
public void after(){
IndexDefinition.setDisableStoredIndexDefinition(false);
}
@Test
public void defaultSetup() throws Exception{
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
assertNotNull(context.getService(QueryIndexProvider.class));
assertNotNull(context.getService(Observer.class));
assertNotNull(context.getService(IndexEditorProvider.class));
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNotNull(editorProvider.getIndexCopier());
assertNotNull(editorProvider.getIndexingQueue());
IndexCopier indexCopier = service.getIndexCopier();
assertNotNull("IndexCopier should be initialized as CopyOnRead is enabled by default", indexCopier);
assertTrue(indexCopier.isPrefetchEnabled());
assertFalse(IndexDefinition.isDisableStoredIndexDefinition());
assertNotNull("CopyOnRead should be enabled by default", context.getService(CopyOnReadStatsMBean.class));
assertNotNull(context.getService(CacheStatsMBean.class));
assertTrue(context.getService(Observer.class) instanceof BackgroundObserver);
assertEquals(InfoStream.NO_OUTPUT, InfoStream.getDefault());
assertEquals(1024, BooleanQuery.getMaxClauseCount());
assertNotNull(FieldUtils.readDeclaredField(service, "documentQueue", true));
assertNotNull(context.getService(JournalPropertyService.class));
assertNotNull(context.getService(IndexImporterProvider.class));
assertNotNull(WhiteboardUtils.getServices(wb, Runnable.class, r -> r instanceof PropertyIndexCleaner));
MockOsgi.deactivate(service, context.bundleContext());
IndexTracker tracker = (IndexTracker) FieldUtils.readDeclaredField(service, "tracker", true);
assertNotNull(tracker.getAsyncIndexInfoService());
}
@Test
public void typeProperty() throws Exception{
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
ServiceReference sr = context.bundleContext().getServiceReference(IndexEditorProvider.class.getName());
assertEquals(TYPE_LUCENE, sr.getProperty("type"));
}
@Test
public void disableOpenIndexAsync() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("enableOpenIndexAsync", false);
MockOsgi.activate(service, context.bundleContext(), config);
assertTrue(context.getService(Observer.class) instanceof LuceneIndexProvider);
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void enableCopyOnWrite() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("enableCopyOnWriteSupport", true);
MockOsgi.activate(service, context.bundleContext(), config);
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNotNull(editorProvider);
assertNotNull(editorProvider.getIndexCopier());
MockOsgi.deactivate(service, context.bundleContext());
}
// OAK-7357
@Test
public void disableCoRCoW() throws Exception {
// inject ds as OAK-7357 revealed ABD bean had a bug - which comes into play only with blob stores
CachingFileDataStore ds = DataStoreUtils
.createCachingFDS(folder.newFolder().getAbsolutePath(),
folder.newFolder().getAbsolutePath());
context.registerService(GarbageCollectableBlobStore.class, new DataStoreBlobStore(ds));
// re-init service and inject references
service = new LuceneIndexProviderService();
MockOsgi.injectServices(service, context.bundleContext());
Map<String,Object> config = getDefaultConfig();
config.put("enableCopyOnReadSupport", false);
config.put("enableCopyOnWriteSupport", false);
// activation should work
MockOsgi.activate(service, context.bundleContext(), config);
// get lucene index provider
LuceneIndexProvider lip = null;
for (QueryIndexProvider qip : context.getServices(QueryIndexProvider.class, null)) {
if (qip instanceof LuceneIndexProvider) {
lip = (LuceneIndexProvider)qip;
break;
}
}
assertNotNull(lip);
IndexTracker tracker = lip.getTracker();
// access reader factory with reflection and implicitly assert that it's DefaultIndexReaderFactory
Field readerFactorFld = IndexTracker.class.getDeclaredField("readerFactory");
readerFactorFld.setAccessible(true);
DefaultIndexReaderFactory readerFactory = (DefaultIndexReaderFactory)readerFactorFld.get(tracker);
Field mipFld = DefaultIndexReaderFactory.class.getDeclaredField("mountInfoProvider");
mipFld.setAccessible(true);
// OAK-7408: LIPS was using default tracker ctor and hence reader factor used default mounts
assertEquals("Reader factory not using configured MountInfoProvider", mip, mipFld.get(readerFactory));
// de-activation should work
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void enablePrefetchIndexFiles() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("prefetchIndexFiles", true);
MockOsgi.activate(service, context.bundleContext(), config);
IndexCopier indexCopier = service.getIndexCopier();
assertTrue(indexCopier.isPrefetchEnabled());
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void debugLogging() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("debug", true);
MockOsgi.activate(service, context.bundleContext(), config);
assertEquals(LoggingInfoStream.INSTANCE, InfoStream.getDefault());
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void enableExtractedTextCaching() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("extractedTextCacheSizeInMB", 11);
MockOsgi.activate(service, context.bundleContext(), config);
ExtractedTextCache textCache = service.getExtractedTextCache();
assertNotNull(textCache.getCacheStats());
assertNotNull(context.getService(CacheStatsMBean.class));
assertEquals(11 * FileUtils.ONE_MB, textCache.getCacheStats().getMaxTotalWeight());
MockOsgi.deactivate(service, context.bundleContext());
assertNull(context.getService(CacheStatsMBean.class));
}
@Test
public void preExtractedTextProvider() throws Exception{
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNull(editorProvider.getExtractedTextCache().getExtractedTextProvider());
assertFalse(editorProvider.getExtractedTextCache().isAlwaysUsePreExtractedCache());
//Mock OSGi does not support components
//context.registerService(PreExtractedTextProvider.class, new DummyProvider());
service.bindExtractedTextProvider(mock(PreExtractedTextProvider.class));
assertNotNull(editorProvider.getExtractedTextCache().getExtractedTextProvider());
}
@Test
public void preExtractedProviderBindBeforeActivate() throws Exception{
service.bindExtractedTextProvider(mock(PreExtractedTextProvider.class));
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNotNull(editorProvider.getExtractedTextCache().getExtractedTextProvider());
}
@Test
public void alwaysUsePreExtractedCache() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("alwaysUsePreExtractedCache", "true");
MockOsgi.activate(service, context.bundleContext(), config);
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertTrue(editorProvider.getExtractedTextCache().isAlwaysUsePreExtractedCache());
}
@Test
public void booleanQuerySize() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("booleanClauseLimit", 4000);
MockOsgi.activate(service, context.bundleContext(), config);
assertEquals(4000, BooleanQuery.getMaxClauseCount());
}
@Test
public void indexDefnStorafe() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("disableStoredIndexDefinition", true);
MockOsgi.activate(service, context.bundleContext(), config);
assertTrue(IndexDefinition.isDisableStoredIndexDefinition());
}
@Test
public void blobStoreRegistered() throws Exception{
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
LuceneIndexEditorProvider editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNull(editorProvider.getBlobStore());
/* Register a blob store */
CachingFileDataStore ds = DataStoreUtils
.createCachingFDS(folder.newFolder().getAbsolutePath(),
folder.newFolder().getAbsolutePath());
context.registerService(GarbageCollectableBlobStore.class, new DataStoreBlobStore(ds));
reactivate();
editorProvider =
(LuceneIndexEditorProvider) context.getService(IndexEditorProvider.class);
assertNotNull(editorProvider.getBlobStore());
}
@Test
public void executorPoolBehaviour() throws Exception{
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
ExecutorService executor = service.getExecutorService();
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
Callable cb1 = new Callable() {
@Override
public Object call() throws Exception {
latch1.await();
return null;
}
};
Callable cb2 = new Callable() {
@Override
public Object call() throws Exception {
latch2.countDown();
return null;
}
};
executor.submit(cb1);
executor.submit(cb2);
//Even if one task gets stuck the other task must get completed
assertTrue("Second task not executed", latch2.await(1, TimeUnit.MINUTES));
latch1.countDown();
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void singleBlobPerIndexFileConfig() throws Exception {
Map<String, Object> config = getDefaultConfig();
config.put("enableSingleBlobIndexFiles", "true");
MockOsgi.activate(service, context.bundleContext(), config);
assertTrue("Enabling property must reflect in BufferedOakDirectory state",
BufferedOakDirectory.isEnableWritingSingleBlobIndexFile());
MockOsgi.deactivate(service, context.bundleContext());
config = getDefaultConfig();
config.put("enableSingleBlobIndexFiles", "false");
MockOsgi.activate(service, context.bundleContext(), config);
assertFalse("Enabling property must reflect in BufferedOakDirectory state",
BufferedOakDirectory.isEnableWritingSingleBlobIndexFile());
MockOsgi.deactivate(service, context.bundleContext());
}
@Test
public void cleanerRegistration() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("propIndexCleanerIntervalInSecs", 142);
MockOsgi.activate(service, context.bundleContext(), config);
ServiceReference[] sr = context.bundleContext().getAllServiceReferences(Runnable.class.getName(),
"(scheduler.name="+ PropertyIndexCleaner.class.getName()+")");
assertEquals(sr.length, 1);
assertEquals(142L, sr[0].getProperty("scheduler.period"));
}
@Test
public void cleanerRegistrationDisabled() throws Exception{
Map<String,Object> config = getDefaultConfig();
config.put("propIndexCleanerIntervalInSecs", 0);
MockOsgi.activate(service, context.bundleContext(), config);
ServiceReference[] sr = context.bundleContext().getAllServiceReferences(Runnable.class.getName(),
"(scheduler.name="+ PropertyIndexCleaner.class.getName()+")");
assertNull(sr);
}
private void reactivate() {
MockOsgi.deactivate(service, context.bundleContext());
service = new LuceneIndexProviderService();
MockOsgi.injectServices(service, context.bundleContext());
MockOsgi.activate(service, context.bundleContext(), getDefaultConfig());
}
private Map<String,Object> getDefaultConfig(){
Map<String,Object> config = new HashMap<String, Object>();
config.put("localIndexDir", folder.getRoot().getAbsolutePath());
return config;
}
}