/* * Hibernate, Relational Persistence for Idiomatic Java * * JBoss, Home of Professional Open Source * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public License, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package org.hibernate.search.indexes.impl; import java.io.IOException; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.lucene.index.IndexReader; import org.apache.lucene.store.Directory; import org.hibernate.annotations.common.AssertionFailure; import org.hibernate.search.SearchException; import org.hibernate.search.indexes.spi.DirectoryBasedReaderProvider; import org.hibernate.search.store.DirectoryProvider; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * This <code>ReaderProvider</code> shares IndexReaders as long as they are "current"; * It uses IndexReader.reopen() which should improve performance on larger indexes * as it shares buffers with previous IndexReader generation for the segments which didn't change. * * @author Sanne Grinovero <sanne@hibernate.org> (C) 2011 Red Hat Inc. */ public class SharingBufferReaderProvider implements DirectoryBasedReaderProvider { private static final Log log = LoggerFactory.make(); /** * contains all Readers (most current per Directory and all unclosed old readers) */ protected final Map<IndexReader, ReaderUsagePair> allReaders = new ConcurrentHashMap<IndexReader, ReaderUsagePair>(); /** * contains last updated Reader; protected by lockOnOpenLatest (in the values) */ protected final Map<Directory, PerDirectoryLatestReader> currentReaders = new ConcurrentHashMap<Directory, PerDirectoryLatestReader>(); /** * Each actual IndexReader refresh will change this value. To what value exactly doesn't matter, * as long as it's not a value recently used, so we don't care for overflow conditions. * When a client needs to check for index freshness a lock is acquired to protect from * too many checks (which result in IO operations); when it is actually able to acquire * this lock it should check if the refreshOperationId changed: if so, a refresh can * be skipped and we release the lock quickly as the IndexReader was then for * sure updated in the time interval between this client arriving to request a reader * and actually being able to get one. */ private volatile int refreshOperationId = 0; private DirectoryProvider directoryProvider; private String indexName; @Override public IndexReader openIndexReader() { log.debugf( "Opening IndexReader for directoryProvider %s", indexName ); Directory directory = directoryProvider.getDirectory(); PerDirectoryLatestReader directoryLatestReader = currentReaders.get( directory ); // might eg happen for FSSlaveDirectoryProvider or for mutable SearchFactory if ( directoryLatestReader == null ) { directoryLatestReader = createReader( directory ); } return directoryLatestReader.refreshAndGet(); } @Override public void closeIndexReader(IndexReader reader) { if ( reader == null ) { return; } log.debugf( "Closing IndexReader: %s", reader ); ReaderUsagePair container = allReaders.get( reader ); container.close(); //virtual } @Override public void initialize(DirectoryBasedIndexManager indexManager, Properties props) { this.directoryProvider = indexManager.getDirectoryProvider(); this.indexName = indexManager.getIndexName(); // Initialize at least one, don't forget directoryProvider might return different Directory later createReader( directoryProvider.getDirectory() ); } /** * Thread safe creation of <code>PerDirectoryLatestReader</code>. * * @param directory The Lucene directory for which to create the reader. * @return either the cached instance for the specified <code>Directory</code> or a newly created one. * @see <a href="http://opensource.atlassian.com/projects/hibernate/browse/HSEARCH-250">HSEARCH-250</a> */ private synchronized PerDirectoryLatestReader createReader(Directory directory) { PerDirectoryLatestReader reader = currentReaders.get( directory ); if ( reader != null ) { return reader; } try { reader = new PerDirectoryLatestReader( directory ); currentReaders.put( directory, reader ); return reader; } catch ( IOException e ) { throw new SearchException( "Unable to open Lucene IndexReader for IndexManager " + this.indexName, e ); } } @Override public void stop() { for ( IndexReader reader : allReaders.keySet() ) { ReaderUsagePair usage = allReaders.get( reader ); usage.close(); } if ( allReaders.size() != 0 ) { log.readersNotProperlyClosedInReaderProvider(); } } //overridable method for testability: protected IndexReader readerFactory(final Directory directory) throws IOException { return IndexReader.open( directory ); } /** * Container for the couple IndexReader,UsageCounter. */ protected final class ReaderUsagePair { public final IndexReader reader; /** * When reaching 0 (always test on change) the reader should be really * closed and then discarded. * Starts at 2 because: * first usage token is artificial: means "current" is not to be closed (+1) * additionally when creating it will be used (+1) */ protected final AtomicInteger usageCounter = new AtomicInteger( 2 ); ReaderUsagePair(IndexReader r) { reader = r; } /** * Closes the <code>IndexReader</code> if no other resource is using it * in which case the reference to this container will also be removed. */ public void close() { int refCount = usageCounter.decrementAndGet(); if ( refCount == 0 ) { //TODO I've been experimenting with the idea of an async-close: didn't appear to have an interesting benefit, //so discarded the code. should try with bigger indexes to see if the effect gets more impressive. ReaderUsagePair removed = allReaders.remove( reader );//remove ourself try { reader.close(); } catch ( IOException e ) { log.unableToCloseLuceneIndexReader( e ); } assert removed != null; } else if ( refCount < 0 ) { //doesn't happen with current code, could help spotting future bugs? throw new AssertionFailure( "Closing an IndexReader for which you didn't own a lock-token, or somebody else which didn't own closed already." ); } } public String toString() { return "Reader:" + this.hashCode() + " ref.count=" + usageCounter.get(); } } /** * An instance for each DirectoryProvider, * establishing the association between "current" ReaderUsagePair * for a DirectoryProvider and it's lock. */ protected final class PerDirectoryLatestReader { /** * Reference to the most current IndexReader for a DirectoryProvider; * guarded by lockOnReplaceCurrent; */ public ReaderUsagePair current; //guarded by lockOnReplaceCurrent private final Lock lockOnReplaceCurrent = new ReentrantLock(); /** * @param directory The <code>Directory</code> for which we manage the <code>IndexReader</code>. * * @throws IOException when the index initialization fails. */ public PerDirectoryLatestReader(Directory directory) throws IOException { IndexReader reader = readerFactory( directory ); ReaderUsagePair initialPair = new ReaderUsagePair( reader ); initialPair.usageCounter.set( 1 ); //a token to mark as active (preventing real close). lockOnReplaceCurrent.lock(); //no harm, just ensuring safe publishing. current = initialPair; lockOnReplaceCurrent.unlock(); allReaders.put( reader, initialPair ); } /** * Gets an updated IndexReader for the current Directory; * the index status will be checked. * * @return the current IndexReader if it's in sync with underlying index, a new one otherwise. */ public IndexReader refreshAndGet() { final IndexReader updatedReader; //it's important that we read this volatile before acquiring the lock: final int preAcquireVersionId = refreshOperationId; ReaderUsagePair toCloseReaderPair = null; lockOnReplaceCurrent.lock(); final IndexReader beforeUpdateReader = current.reader; try { if ( refreshOperationId != preAcquireVersionId ) { // We can take a good shortcut current.usageCounter.incrementAndGet(); return beforeUpdateReader; } else { try { //Guarded by the lockOnReplaceCurrent of current IndexReader //technically the final value doesn't even matter, as long as we change it refreshOperationId++; updatedReader = IndexReader.openIfChanged( beforeUpdateReader ); } catch ( IOException e ) { throw new SearchException( "Unable to reopen IndexReader", e ); } } if ( updatedReader == null ) { current.usageCounter.incrementAndGet(); return beforeUpdateReader; } else { ReaderUsagePair newPair = new ReaderUsagePair( updatedReader ); //no need to increment usageCounter in newPair, as it is constructed with correct number 2. assert newPair.usageCounter.get() == 2; toCloseReaderPair = current; current = newPair; allReaders.put( updatedReader, newPair );//unfortunately still needs lock } } finally { lockOnReplaceCurrent.unlock(); } // doesn't need lock: if ( toCloseReaderPair != null ) { toCloseReaderPair.close();// release a token as it's not the current any more. } return updatedReader; } } }