/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat, Inc. * * 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, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY 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 * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.hibernate.search.store.impl; import java.io.File; import java.io.IOException; import java.util.Properties; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import org.apache.lucene.store.FSDirectory; import org.hibernate.search.indexes.impl.DirectoryBasedIndexManager; import org.hibernate.search.store.DirectoryProvider; import org.hibernate.search.util.impl.FileHelper; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.spi.BuildContext; import org.hibernate.search.SearchException; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * File based DirectoryProvider that takes care of index copy * The base directory is represented by hibernate.search.<index>.indexBase * The index is created in <base directory>/<index name> * The source (aka copy) directory is built from <sourceBase>/<index name> * * A copy is triggered every refresh seconds * * @author Emmanuel Bernard * @author Sanne Grinovero */ //TODO rename copy? public class FSMasterDirectoryProvider implements DirectoryProvider<FSDirectory> { private static final String CURRENT1 = "current1"; private static final String CURRENT2 = "current2"; // defined to have CURRENT_DIR_NAME[1] == "current"+"1": private static final String[] CURRENT_DIR_NAME = { null, CURRENT1, CURRENT2 }; private static final Log log = LoggerFactory.make(); private final Timer timer = new Timer( true ); //daemon thread, the copy algorithm is robust private volatile int current; //variables having visibility granted by a read of "current" private FSDirectory directory; private String indexName; private long copyChunkSize; //variables needed between initialize and start (used by same thread: no special care needed) private File sourceDir; private File indexDir; private String directoryProviderName; private Properties properties; private TriggerTask task; private Lock directoryProviderLock; @Override public void initialize(String directoryProviderName, Properties properties, BuildContext context) { this.properties = properties; this.directoryProviderName = directoryProviderName; //source guessing sourceDir = DirectoryProviderHelper.getSourceDirectory( directoryProviderName, properties, true ); log.debugf( "Source directory: %s", sourceDir.getPath() ); indexDir = DirectoryProviderHelper.getVerifiedIndexDir( directoryProviderName, properties, true ); log.debugf( "Index directory: %s", indexDir.getPath() ); try { indexName = indexDir.getCanonicalPath(); directory = DirectoryProviderHelper.createFSIndex( indexDir, properties ); } catch (IOException e) { throw new SearchException( "Unable to initialize index: " + directoryProviderName, e ); } copyChunkSize = DirectoryProviderHelper.getCopyBufferSize( directoryProviderName, properties ); current = 0; //write to volatile to publish all state } @Override public void start(DirectoryBasedIndexManager indexManager) { int currentLocal = 0; this.directoryProviderLock = indexManager.getDirectoryModificationLock(); try { //copy to source if ( new File( sourceDir, CURRENT1 ).exists() ) { currentLocal = 2; } else if ( new File( sourceDir, CURRENT2 ).exists() ) { currentLocal = 1; } else { log.debugf( "Source directory for '%s' will be initialized", indexName); currentLocal = 1; } String currentString = Integer.valueOf( currentLocal ).toString(); File subDir = new File( sourceDir, currentString ); FileHelper.synchronize( indexDir, subDir, true, copyChunkSize ); new File( sourceDir, CURRENT1 ).delete(); new File( sourceDir, CURRENT2 ).delete(); //TODO small hole, no file can be found here new File( sourceDir, CURRENT_DIR_NAME[currentLocal] ).createNewFile(); log.debugf( "Current directory: %d", currentLocal ); } catch (IOException e) { throw new SearchException( "Unable to initialize index: " + directoryProviderName, e ); } task = new FSMasterDirectoryProvider.TriggerTask( indexDir, sourceDir ); long period = DirectoryProviderHelper.getRefreshPeriod( properties, directoryProviderName ); timer.scheduleAtFixedRate( task, period, period ); this.current = currentLocal; //write to volatile to publish all state } public FSDirectory getDirectory() { @SuppressWarnings("unused") int readCurrentState = current; //Unneeded value, needed to ensure visibility of state protected by memory barrier return directory; } @Override public boolean equals(Object obj) { // this code is actually broken since the value change after initialize call // but from a practical POV this is fine since we only call this method // after initialize call if ( obj == this ) return true; if ( obj == null || !( obj instanceof FSMasterDirectoryProvider ) ) return false; FSMasterDirectoryProvider other = (FSMasterDirectoryProvider)obj; //break both memory barriers by reading volatile variables: @SuppressWarnings("unused") int readCurrentState = other.current; readCurrentState = this.current; return indexName.equals( other.indexName ); } @Override public int hashCode() { // this code is actually broken since the value change after initialize call // but from a practical POV this is fine since we only call this method // after initialize call @SuppressWarnings("unused") int readCurrentState = current; //Unneeded value, to ensure visibility of state protected by memory barrier int hash = 11; return 37 * hash + indexName.hashCode(); } public void stop() { @SuppressWarnings("unused") int readCurrentState = current; //Another unneeded value, to ensure visibility of state protected by memory barrier timer.cancel(); task.stop(); try { directory.close(); } catch (Exception e) { log.unableToCloseLuceneDirectory( directory.getDirectory(), e ); } } private class TriggerTask extends TimerTask { private final ExecutorService executor; private final FSMasterDirectoryProvider.CopyDirectory copyTask; public TriggerTask(File source, File destination) { executor = Executors.newSingleThreadExecutor(); copyTask = new FSMasterDirectoryProvider.CopyDirectory( source, destination ); } public void run() { if ( copyTask.inProgress.compareAndSet( false, true ) ) { executor.execute( copyTask ); } else { log.skippingDirectorySynchronization( indexName ); } } public void stop() { executor.shutdownNow(); } } private class CopyDirectory implements Runnable { private final File source; private final File destination; private final AtomicBoolean inProgress = new AtomicBoolean( false ); public CopyDirectory(File source, File destination) { this.source = source; this.destination = destination; } public void run() { //TODO get rid of current and use the marker file instead? directoryProviderLock.lock(); try { long start = System.nanoTime();//keep time after lock is acquired for correct measure int oldIndex = current; int index = oldIndex == 1 ? 2 : 1; File destinationFile = new File( destination, Integer.valueOf(index).toString() ); try { log.tracef( "Copying %s into %s", source, destinationFile ); FileHelper.synchronize( source, destinationFile, true, copyChunkSize ); current = index; } catch (IOException e) { //don't change current log.unableToSynchronizeSource( indexName, e ); return; } if ( ! new File( destination, CURRENT_DIR_NAME[oldIndex] ).delete() ) { log.unableToRemovePreviousMarket( indexName ); } try { new File( destination, CURRENT_DIR_NAME[index] ).createNewFile(); } catch( IOException e ) { log.unableToCreateCurrentMarker( indexName, e ); } log.tracef( "Copy for %s took %d ms", indexName, TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - start ) ); } finally { directoryProviderLock.unlock(); inProgress.set( false ); } } } }