/*
* 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 );
}
}
}
}