/* * 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 org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.RAMDirectory; import org.hibernate.search.indexes.impl.DirectoryBasedIndexManager; import org.hibernate.search.spi.BuildContext; import org.hibernate.search.store.DirectoryProvider; import org.hibernate.search.util.configuration.impl.ConfigurationParseHelper; import org.hibernate.search.util.impl.FileHelper; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.annotations.common.AssertionFailure; import org.hibernate.search.Environment; import org.hibernate.search.SearchException; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * File based directory provider that takes care of getting a version of the index * from a given source. * 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> * <p/> * A copy is triggered every refresh seconds * * @author Emmanuel Bernard * @author Sanne Grinovero * @author Oliver Siegmar */ public class FSSlaveDirectoryProvider implements DirectoryProvider<Directory> { private static final Log log = LoggerFactory.make(); private final Timer timer = new Timer( true ); //daemon thread, the copy algorithm is robust private volatile boolean initialized = false; private volatile boolean started = false; private volatile int current; //used also as memory barrier of all other values, which are set once. //variables having visibility granted by a read of "current" private volatile Directory dummyDirectory; private FSDirectory directory1; private FSDirectory directory2; private String indexName; private long copyChunkSize; //variables needed between initialize and start (used by same thread: no special care needed) private File sourceIndexDir; private File indexDir; private String directoryProviderName; private Properties properties; private UpdateTask updateTask; @Override public void initialize(String directoryProviderName, Properties properties, BuildContext context) { this.properties = properties; this.directoryProviderName = directoryProviderName; //source guessing sourceIndexDir = DirectoryProviderHelper.getSourceDirectory( directoryProviderName, properties, false ); log.debugf( "Source directory: %s", sourceIndexDir.getPath() ); indexDir = DirectoryProviderHelper.getVerifiedIndexDir( directoryProviderName, properties, true ); log.debugf( "Index directory: %s", indexDir.getPath() ); try { indexName = indexDir.getCanonicalPath(); } catch ( IOException e ) { throw new SearchException( "Unable to initialize index: " + directoryProviderName, e ); } copyChunkSize = DirectoryProviderHelper.getCopyBufferSize( directoryProviderName, properties ); current = 0; //publish all state to other threads } private boolean currentMarkerIsInSource() { int retry = ConfigurationParseHelper.getIntValue( properties, Environment.RETRY_MARKER_LOOKUP, 0 ); if ( retry < 0 ) { throw new SearchException( Environment.RETRY_MARKER_LOOKUP + " option must be a positive integer, but was \"" + retry + "\"" ); } boolean currentMarkerInSource = false; for ( int tried = 0 ; tried <= retry ; tried++ ) { //we try right away the first time if ( tried > 0) { try { Thread.sleep( TimeUnit.SECONDS.toMillis( 5 ) ); } catch ( InterruptedException e ) { //continue Thread.currentThread().interrupt(); } } currentMarkerInSource = new File( sourceIndexDir, "current1" ).exists() || new File( sourceIndexDir, "current2" ).exists(); if ( currentMarkerInSource ) { break; } } return currentMarkerInSource; } @Override public void start(DirectoryBasedIndexManager indexManager) { if ( ! attemptInitializeAndStart() ) { // if we failed to initialize and/or start, we'll try again later: setup a timer long period = DirectoryProviderHelper.getRetryInitializePeriod( properties, directoryProviderName ); if ( period != 0 ) { scheduleTask( new InitTask(), period ); } else { throw new SearchException( "Failed to initialize DirectoryProvider \"" + directoryProviderName + "\": could not find marker file in index source" ); } } } private void startIt() { @SuppressWarnings("unused") int readCurrentState = current; //Unneeded value, but ensure visibility of state protected by memory barrier int currentToBe = 0; try { directory1 = DirectoryProviderHelper.createFSIndex( new File( indexDir, "1" ), properties ); directory2 = DirectoryProviderHelper.createFSIndex( new File( indexDir, "2" ), properties ); File currentMarker = new File( indexDir, "current1" ); File current2Marker = new File( indexDir, "current2" ); if ( currentMarker.exists() ) { currentToBe = 1; if ( current2Marker.exists() ) { current2Marker.delete(); //TODO or throw an exception? } } else if ( current2Marker.exists() ) { currentToBe = 2; } else { //no default log.debug( "Setting directory 1 as current" ); currentToBe = 1; File destinationFile = new File( indexDir, Integer.valueOf( currentToBe ).toString() ); int sourceCurrent; if ( new File( sourceIndexDir, "current1" ).exists() ) { sourceCurrent = 1; } else if ( new File( sourceIndexDir, "current2" ).exists() ) { sourceCurrent = 2; } else { throw new SearchException( "No current file marker found in source directory: " + sourceIndexDir.getPath() ); } try { FileHelper.synchronize( new File( sourceIndexDir, String.valueOf( sourceCurrent ) ), destinationFile, true, copyChunkSize ); } catch ( IOException e ) { throw new SearchException( "Unable to synchronize directory: " + indexName, e ); } if ( !currentMarker.createNewFile() ) { throw new SearchException( "Unable to create the directory marker file: " + indexName ); } } log.debugf( "Current directory: %d", currentToBe ); } catch ( IOException e ) { throw new SearchException( "Unable to initialize index: " + directoryProviderName, e ); } updateTask = new UpdateTask( sourceIndexDir, indexDir ); long period = DirectoryProviderHelper.getRefreshPeriod( properties, directoryProviderName ); scheduleTask( updateTask, period ); this.current = currentToBe; started = true; } public Directory getDirectory() { if ( !started ) { if ( dummyDirectory == null ) { RAMDirectory directory = new RAMDirectory(); DirectoryProviderHelper.initializeIndexIfNeeded( directory ); dummyDirectory = directory; } return dummyDirectory; } int readState = current;// to have the read consistent in the next two "if"s. if ( readState == 1 ) { return directory1; } else if ( readState == 2 ) { return directory2; } else { throw new AssertionFailure( "Illegal current directory: " + readState ); } } @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 FSSlaveDirectoryProvider ) ) { return false; } FSSlaveDirectoryProvider other = ( FSSlaveDirectoryProvider ) obj; //need to break memory barriers on both instances: @SuppressWarnings("unused") int readCurrentState = this.current; //unneeded value, but ensure visibility of indexName readCurrentState = other.current; //another unneeded value, but ensure visibility of indexName 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, but ensure visibility of indexName int hash = 11; return 37 * hash + indexName.hashCode(); } class InitTask extends TimerTask { @Override public void run() { try { if ( attemptInitializeAndStart() ) { // then this task is no longer needed cancel(); } } catch (RuntimeException re) { // we need this to make sure the error is logged somewhere, // as we're executing it in the timer thread log.failedSlaveDirectoryProviderInitialization( indexName, re ); } } } /** * @return true if both initialize and start succeeded */ protected synchronized boolean attemptInitializeAndStart() { if ( !initialized ) { if ( currentMarkerIsInSource() ) { initialized = true; log.foundCurrentMarker(); } else { log.noCurrentMarkerInSourceDirectory(); } } if ( initialized ) { startIt(); } return this.started; } class UpdateTask extends TimerTask { private final ExecutorService executor; private final CopyDirectory copyTask; public UpdateTask(File sourceIndexDir, File destination) { executor = Executors.newSingleThreadExecutor(); copyTask = new CopyDirectory( sourceIndexDir, destination ); } public void run() { if ( copyTask.inProgress.compareAndSet( false, true ) ) { executor.execute( copyTask ); } else { if ( log.isTraceEnabled() ) { @SuppressWarnings("unused") int unneeded = current;//ensure visibility of indexName in Timer threads. log.tracef( "Skipping directory synchronization, previous work still in progress: %s", indexName ); } } } public void stop() { executor.shutdownNow(); } } class CopyDirectory implements Runnable { private final File source; private final File destination; private final AtomicBoolean inProgress = new AtomicBoolean( false ); public CopyDirectory(File sourceIndexDir, File destination) { this.source = sourceIndexDir; this.destination = destination; } public void run() { long start = System.nanoTime(); try { File sourceFile = determineCurrentSourceFile(); if ( sourceFile == null ) { log.unableToDetermineCurrentInSourceDirectory(); return; } // check whether a copy is needed at all File currentDestinationFile = new File( destination, Integer.valueOf( current ).toString() ); try { if ( FileHelper.areInSync( sourceFile, currentDestinationFile ) ) { if ( log.isTraceEnabled() ) { log.trace( "Source and destination directory are in sync. No copying required." ); } return; } } catch ( IOException ioe ) { log.unableToCompareSourceWithDestinationDirectory( sourceFile.getName(), currentDestinationFile.getName() ); } // copy is required 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", sourceFile, destinationFile ); FileHelper.synchronize( sourceFile, destinationFile, true, copyChunkSize ); current = index; log.tracef( "Copy for %s took %d ms", indexName, TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - start ) ); } catch ( IOException e ) { //don't change current log.unableToSynchronizeSource( indexName, e ); return; } if ( !new File( indexName, "current" + oldIndex ).delete() ) { log.unableToRemovePreviousMarket( indexName ); } try { new File( indexName, "current" + index ).createNewFile(); } catch ( IOException e ) { log.unableToCreateCurrentMarker( indexName, e ); } } finally { inProgress.set( false ); } } /** * @return Return a file to the currently active source directory. Tests for the files "current1" and * "current2" in order to determine which is the current directory. If there marker file does not exists * <code>null</code> is returned. */ private File determineCurrentSourceFile() { File sourceFile = null; if ( new File( source, "current1" ).exists() ) { sourceFile = new File( source, "1" ); } else if ( new File( source, "current2" ).exists() ) { sourceFile = new File( source, "2" ); } return sourceFile; } } public void stop() { @SuppressWarnings("unused") int readCurrentState = current; //unneeded value, but ensure visibility of state protected by memory barrier timer.cancel(); if ( updateTask != null ) { updateTask.stop(); } closeDirectory( directory1 ); closeDirectory( directory2 ); } private void closeDirectory(Directory directory) { if ( directory != null ) { try { directory.close(); } catch ( Exception e ) { log.unableToCloseLuceneDirectory( directory, e ); } } } protected void scheduleTask(TimerTask task, long period) { timer.schedule( task, period, period ); } }