/*
* 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.test.engine;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.FilterDirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.AssertingIndexSearcher;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.QueryCache;
import org.apache.lucene.search.QueryCachingPolicy;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.engine.EngineConfig;
import org.elasticsearch.index.engine.EngineException;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.test.ESIntegTestCase;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.IdentityHashMap;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Support class to build MockEngines like {@link org.elasticsearch.test.engine.MockInternalEngine}
* since they need to subclass the actual engine
*/
public final class MockEngineSupport {
/**
* Allows tests to wrap an index reader randomly with a given ratio. This is disabled by default ie. <tt>0.0d</tt> since reader wrapping is insanely
* slow if {@link org.apache.lucene.index.AssertingDirectoryReader} is used.
*/
public static final Setting<Double> WRAP_READER_RATIO =
Setting.doubleSetting("index.engine.mock.random.wrap_reader_ratio", 0.0d, 0.0d, Property.IndexScope);
/**
* Allows tests to prevent an engine from being flushed on close ie. to test translog recovery...
*/
public static final Setting<Boolean> DISABLE_FLUSH_ON_CLOSE =
Setting.boolSetting("index.mock.disable_flush_on_close", false, Property.IndexScope);
private final AtomicBoolean closing = new AtomicBoolean(false);
private final Logger logger = Loggers.getLogger(Engine.class);
private final ShardId shardId;
private final QueryCache filterCache;
private final QueryCachingPolicy filterCachingPolicy;
private final SearcherCloseable searcherCloseable;
private final MockContext mockContext;
private final boolean disableFlushOnClose;
public boolean isFlushOnCloseDisabled() {
return disableFlushOnClose;
}
public static class MockContext {
private final Random random;
private final boolean wrapReader;
private final Class<? extends FilterDirectoryReader> wrapper;
private final Settings indexSettings;
public MockContext(Random random, boolean wrapReader, Class<? extends FilterDirectoryReader> wrapper, Settings indexSettings) {
this.random = random;
this.wrapReader = wrapReader;
this.wrapper = wrapper;
this.indexSettings = indexSettings;
}
}
public MockEngineSupport(EngineConfig config, Class<? extends FilterDirectoryReader> wrapper) {
Settings settings = config.getIndexSettings().getSettings();
shardId = config.getShardId();
filterCache = config.getQueryCache();
filterCachingPolicy = config.getQueryCachingPolicy();
final long seed = config.getIndexSettings().getValue(ESIntegTestCase.INDEX_TEST_SEED_SETTING);
Random random = new Random(seed);
final double ratio = WRAP_READER_RATIO.get(settings);
boolean wrapReader = random.nextDouble() < ratio;
if (logger.isTraceEnabled()) {
logger.trace("Using [{}] for shard [{}] seed: [{}] wrapReader: [{}]", this.getClass().getName(), shardId, seed, wrapReader);
}
mockContext = new MockContext(random, wrapReader, wrapper, settings);
this.searcherCloseable = new SearcherCloseable();
LuceneTestCase.closeAfterSuite(searcherCloseable); // only one suite closeable per Engine
this.disableFlushOnClose = DISABLE_FLUSH_ON_CLOSE.get(settings);
}
enum CloseAction {
FLUSH_AND_CLOSE,
CLOSE;
}
/**
* Returns the CloseAction to execute on the actual engine. Note this method changes the state on
* the first call and treats subsequent calls as if the engine passed is already closed.
*/
public CloseAction flushOrClose(CloseAction originalAction) throws IOException {
if (closing.compareAndSet(false, true)) { // only do the random thing if we are the first call to this since super.flushOnClose() calls #close() again and then we might end up with a stackoverflow.
if (mockContext.random.nextBoolean()) {
return CloseAction.FLUSH_AND_CLOSE;
} else {
return CloseAction.CLOSE;
}
} else {
return originalAction;
}
}
public AssertingIndexSearcher newSearcher(String source, IndexSearcher searcher, SearcherManager manager) throws EngineException {
IndexReader reader = searcher.getIndexReader();
IndexReader wrappedReader = reader;
assert reader != null;
if (reader instanceof DirectoryReader && mockContext.wrapReader) {
wrappedReader = wrapReader((DirectoryReader) reader);
}
// this executes basic query checks and asserts that weights are normalized only once etc.
final AssertingIndexSearcher assertingIndexSearcher = new AssertingIndexSearcher(mockContext.random, wrappedReader);
assertingIndexSearcher.setSimilarity(searcher.getSimilarity(true));
assertingIndexSearcher.setQueryCache(filterCache);
assertingIndexSearcher.setQueryCachingPolicy(filterCachingPolicy);
return assertingIndexSearcher;
}
private DirectoryReader wrapReader(DirectoryReader reader) {
try {
Constructor<?>[] constructors = mockContext.wrapper.getConstructors();
Constructor<?> nonRandom = null;
for (Constructor<?> constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length > 0 && parameterTypes[0] == DirectoryReader.class) {
if (parameterTypes.length == 1) {
nonRandom = constructor;
} else if (parameterTypes.length == 2 && parameterTypes[1] == Settings.class) {
return (DirectoryReader) constructor.newInstance(reader, mockContext.indexSettings);
}
}
}
if (nonRandom != null) {
return (DirectoryReader) nonRandom.newInstance(reader);
}
} catch (Exception e) {
throw new ElasticsearchException("Can not wrap reader", e);
}
return reader;
}
public abstract static class DirectoryReaderWrapper extends FilterDirectoryReader {
protected final SubReaderWrapper subReaderWrapper;
public DirectoryReaderWrapper(DirectoryReader in, SubReaderWrapper subReaderWrapper) throws IOException {
super(in, subReaderWrapper);
this.subReaderWrapper = subReaderWrapper;
}
}
public Engine.Searcher wrapSearcher(String source, Engine.Searcher engineSearcher, IndexSearcher searcher, SearcherManager manager) {
final AssertingIndexSearcher assertingIndexSearcher = newSearcher(source, searcher, manager);
assertingIndexSearcher.setSimilarity(searcher.getSimilarity(true));
// pass the original searcher to the super.newSearcher() method to make sure this is the searcher that will
// be released later on. If we wrap an index reader here must not pass the wrapped version to the manager
// on release otherwise the reader will be closed too early. - good news, stuff will fail all over the place if we don't get this right here
AssertingSearcher assertingSearcher = new AssertingSearcher(assertingIndexSearcher, engineSearcher, shardId, logger) {
@Override
public void close() {
try {
searcherCloseable.remove(this);
} finally {
super.close();
}
}
};
searcherCloseable.add(assertingSearcher, engineSearcher.source());
return assertingSearcher;
}
private static final class SearcherCloseable implements Closeable {
private final IdentityHashMap<AssertingSearcher, RuntimeException> openSearchers = new IdentityHashMap<>();
@Override
public synchronized void close() throws IOException {
if (openSearchers.isEmpty() == false) {
AssertionError error = new AssertionError("Unreleased searchers found");
for (RuntimeException ex : openSearchers.values()) {
error.addSuppressed(ex);
}
throw error;
}
}
void add(AssertingSearcher searcher, String source) {
final RuntimeException ex = new RuntimeException("Unreleased Searcher, source [" + source+ "]");
synchronized (this) {
openSearchers.put(searcher, ex);
}
}
synchronized void remove(AssertingSearcher searcher) {
openSearchers.remove(searcher);
}
}
}