/*
* Copyright (c) 2002-2009 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.impl.nioneo.store;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Map;
import java.util.logging.Logger;
import org.neo4j.kernel.Config;
import org.neo4j.kernel.impl.core.ReadOnlyDbException;
/**
* Contains common implementation for {@link AbstractStore} and
* {@link AbstractDynamicStore}.
*/
public abstract class CommonAbstractStore
{
protected static final Logger logger = Logger
.getLogger( CommonAbstractStore.class.getName() );
/**
* Returns the type and version that identifies this store.
*
* @return This store's implementation type and version identifier
*/
public abstract String getTypeAndVersionDescriptor();
/**
* Called from the constructor after the end header has been checked. The
* store implementation can setup it's
* {@link PersistenceWindow persistence windows} and other resources that
* are needed by overriding this implementation.
* <p>
* This default implementation does nothing.
*
* @throws IOException
* If unable to initialize
*/
protected void initStorage()
{
}
protected boolean versionFound( String version )
{
return true;
}
/**
* This method should close/release all resources that the implementation of
* this store has allocated and is called just before the <CODE>close()</CODE>
* method returns. Override this method to clean up stuff created in
* {@link #initStorage()} method.
* <p>
* This default implementation does nothing.
*
* @throws IOException
* If unable to close
*/
protected void closeStorage()
{
};
/**
* Should do first validation on store validating stuff like version and id
* generator. This method is called by constructors.
*
* @throws IOException
* If unable to load store
*/
protected abstract void loadStorage();
/**
* Should rebuild the id generator from scratch.
*
* @throws IOException
* If unable to rebuild id generator.
*/
protected abstract void rebuildIdGenerator();
// default node store id generator grab size
protected static final int DEFAULT_ID_GRAB_SIZE = 1024;
private final String storageFileName;
private IdGenerator idGenerator = null;
private FileChannel fileChannel = null;
private PersistenceWindowPool windowPool;
private boolean storeOk = true;
private FileLock fileLock;
private boolean grabFileLock = true;
private Map<?,?> config = null;
private boolean readOnly = false;
private boolean backupSlave = false;
/**
* Opens and validates the store contained in <CODE>fileName</CODE>
* loading any configuration defined in <CODE>config</CODE>. After
* validation the <CODE>initStorage</CODE> method is called.
* <p>
* If the store had a clean shutdown it will be marked as <CODE>ok</CODE>
* and the {@link #validate()} method will not throw exception when invoked.
* If a problem was found when opening the store the {@link #makeStoreOk()}
* must be invoked else {@link #validate()} will throw exception.
*
* throws IOException if the unable to open the storage or if the
* <CODE>initStorage</CODE> method fails
*
* @param fileName
* The name of the store
* @param config
* The configuration for store (may be null)
* @throws IOException
* If store doesn't exist
*/
public CommonAbstractStore( String fileName, Map<?,?> config )
{
this.storageFileName = fileName;
this.config = config;
if ( config != null )
{
String fileLock = (String) config.get( "grab_file_lock" );
if ( fileLock != null && fileLock.toLowerCase().equals( "false" ) )
{
grabFileLock = false;
}
}
checkStorage();
loadStorage();
initStorage();
}
boolean isReadOnly()
{
return readOnly;
}
boolean isBackupSlave()
{
return backupSlave;
}
/**
* Opens and validates the store contained in <CODE>fileName</CODE>.
* After validation the <CODE>initStorage</CODE> method is called.
* <p>
* If the store had a clean shutdown it will be marked as <CODE>ok</CODE>
* and the {@link #validate()} method will not throw exception when invoked.
* If a problem was found when opening the store the {@link #makeStoreOk()}
* must be invoked else {@link #validate()} will throw exception.
*
* throws IOException if the unable to open the storage or if the
* <CODE>initStorage</CODE> method fails
*
* @param fileName
* The name of the store
* @throws IOException
* If store doesn't exist
*/
public CommonAbstractStore( String fileName )
{
this.storageFileName = fileName;
checkStorage();
loadStorage();
initStorage();
}
private void checkStorage()
{
if ( config != null )
{
Boolean isReadOnly = (Boolean) config.get( "read_only" );
if ( isReadOnly != null )
{
readOnly = isReadOnly;
}
}
if ( config != null )
{
String str = (String) config.get( "backup_slave" );
if ( "true".equals( str ) )
{
backupSlave = true;
}
}
if ( !new File( storageFileName ).exists() )
{
throw new IllegalStateException( "No such store[" + storageFileName
+ "]" );
}
try
{
if ( !readOnly || backupSlave )
{
this.fileChannel = new RandomAccessFile( storageFileName, "rw" )
.getChannel();
}
else
{
this.fileChannel = new RandomAccessFile( storageFileName, "r" )
.getChannel();
}
}
catch ( IOException e )
{
throw new UnderlyingStorageException( "Unable to open file "
+ storageFileName, e );
}
try
{
if ( (!readOnly || backupSlave) && grabFileLock )
{
this.fileLock = this.fileChannel.tryLock();
if ( fileLock == null )
{
fileChannel.close();
throw new IllegalStateException( "Unable to lock store ["
+ storageFileName + "], this is usually a result of some "
+ "other Neo4j kernel running using the same store." );
}
}
}
catch ( IOException e )
{
throw new UnderlyingStorageException( "Unable to lock store["
+ storageFileName + "]" );
}
}
/**
* Marks this store as "not ok".
*
*/
protected void setStoreNotOk()
{
if ( readOnly && !isBackupSlave() )
{
throw new UnderlyingStorageException(
"Cannot start up on non clean store as read only" );
}
storeOk = false;
}
/**
* If store is "not ok" <CODE>false</CODE> is returned.
*
* @return True if this store is ok
*/
protected boolean getStoreOk()
{
return storeOk;
}
/**
* Sets the {@link PersistenceWindowPool} for this store to use. Normally
* this is set in the {@link #loadStorage()} method. This method must be
* invoked with a valid "pool" before any of the
* {@link #acquireWindow(int, OperationType)}
* {@link #releaseWindow(PersistenceWindow)} {@link #flush(int)}
* {@link #forget(int)} {@link #close()} methods are invoked.
*
* @param pool
* The window pool this store should use
*/
protected void setWindowPool( PersistenceWindowPool pool )
{
this.windowPool = pool;
}
/**
* Returns the next id for this store's {@link IdGenerator}.
*
* @return The next free id
* @throws IOException
* If unable to get next free id
*/
public int nextId()
{
return (int) idGenerator.nextId();
}
/**
* Frees an id for this store's {@link IdGenerator}.
*
* @param id
* The id to free
* @throws IOException
* If unable to free the id
*/
public void freeId( int id )
{
idGenerator.freeId( makeUnsignedInt( id ) );
}
private long makeUnsignedInt( int signedInteger )
{
return signedInteger & 0xFFFFFFFFL;
}
/**
* Return the highest id in use.
*
* @return The highest id in use.
*/
protected long getHighId()
{
return idGenerator.getHighId();
}
/**
* Sets the highest id in use (use this when rebuilding id generator).
*
* @param highId
* The high id to set.
*/
protected void setHighId( long highId )
{
if ( idGenerator != null )
{
idGenerator.setHighId( highId );
}
}
protected boolean getIfMemoryMapped()
{
if ( getConfig() != null )
{
String useMemMapped = (String) getConfig().get(
Config.USE_MEMORY_MAPPED_BUFFERS );
if ( useMemMapped != null &&
useMemMapped.toLowerCase().equals( "false" ) )
{
return false;
}
}
return true;
}
/**
* Returns memory assigned for
* {@link MappedPersistenceWindow memory mapped windows} in bytes. The
* configuration map passed in one constructor is checked for an entry with
* this stores name.
*
* @return The number of bytes memory mapped windows this store has
*/
protected long getMappedMem()
{
if ( getConfig() != null )
{
String convertSlash = storageFileName.replace( '\\', '/' );
String realName = convertSlash.substring( convertSlash
.lastIndexOf( '/' ) + 1 );
String mem = (String) getConfig().get( realName + ".mapped_memory" );
if ( mem != null )
{
long multiplier = 1;
if ( mem.endsWith( "M" ) )
{
multiplier = 1024 * 1024;
mem = mem.substring( 0, mem.length() - 1 );
}
else if ( mem.endsWith( "k" ) )
{
multiplier = 1024;
mem = mem.substring( 0, mem.length() - 1 );
}
else if ( mem.endsWith( "G" ) )
{
multiplier = 1024*1024*1024;
mem = mem.substring( 0, mem.length() - 1 );
}
try
{
return Integer.parseInt( mem ) * multiplier;
}
catch ( NumberFormatException e )
{
logger.info( "Unable to parse mapped memory[" + mem
+ "] string for " + storageFileName );
}
}
}
return 0;
}
/**
* If store is not ok a call to this method will rebuild the {@link
* IdGenerator} used by this store and if successful mark it as
* <CODE>ok</CODE>.
*
* @throws IOException
* If unable to rebuild id generator
*/
public void makeStoreOk()
{
if ( !storeOk )
{
if ( readOnly && !backupSlave )
{
throw new ReadOnlyDbException();
}
rebuildIdGenerator();
storeOk = true;
}
}
public void rebuildIdGenerators()
{
if ( readOnly && !backupSlave )
{
throw new ReadOnlyDbException();
}
rebuildIdGenerator();
}
/**
* Returns the configuration map if set in constructor.
*
* @return A map containing configuration or <CODE>null<CODE> if no
* configuration map set.
*/
public Map<?,?> getConfig()
{
return config;
}
/**
* Acquires a {@link PersistenceWindow} for <CODE>position</CODE> and
* operation <CODE>type</CODE>. Window must be released after operation
* has been performed via {@link #releaseWindow(PersistenceWindow)}.
*
* @param position
* The record position
* @param type
* The operation type
* @return a persistence window encapsulating the record
* @throws IOException
* If unable to acquire window
*/
protected PersistenceWindow acquireWindow( int sPosition, OperationType type )
{
long position = makeUnsignedInt( sPosition );
if ( !isInRecoveryMode()
&& ( position > idGenerator.getHighId() || !storeOk) )
{
throw new InvalidRecordException( "Position[" + position
+ "] requested for operation is high id["
+ idGenerator.getHighId() + "] or store is flagged as dirty["
+ storeOk + "]" );
}
return windowPool.acquire( position, type );
}
/**
* Releases the window and writes the data (async) if the
* <CODE>window</CODE> was a {@link PersistenceRow}.
*
* @param window
* The window to be released
* @throws IOException
* If window was a <CODE>DirectPersistenceRow</CODE> and
* unable to write out its data to the store
*/
protected void releaseWindow( PersistenceWindow window )
{
windowPool.release( window );
}
public void flushAll()
{
windowPool.flushAll();
}
private boolean isRecovered = false;
protected boolean isInRecoveryMode()
{
return isRecovered;
}
protected void setRecovered()
{
isRecovered = true;
}
protected void unsetRecovered()
{
isRecovered = false;
}
/**
* Returns the name of this store.
*
* @return The name of this store
*/
public String getStorageFileName()
{
return storageFileName;
}
/**
* Opens the {@link IdGenerator} used by this store.
*
* @throws IOException
* If unable to open the id generator
*/
protected void openIdGenerator()
{
idGenerator = new IdGeneratorImpl( storageFileName + ".id",
DEFAULT_ID_GRAB_SIZE );
}
protected void openReadOnlyIdGenerator( int recordSize )
{
try
{
idGenerator = new ReadOnlyIdGenerator( storageFileName + ".id",
fileChannel.size() / recordSize );
}
catch ( IOException e )
{
throw new UnderlyingStorageException( e );
}
}
/**
* Closed the {@link IdGenerator} used by this store
*
* @throws IOException
* If unable to close this store
*/
protected void closeIdGenerator()
{
if ( idGenerator != null )
{
idGenerator.close();
}
}
/**
* Closes this store. This will cause all buffers and channels to be closed.
* Requesting an operation from after this method has been invoked is
* illegal and an exception will be thrown.
* <p>
* This method will start by invoking the {@link #closeStorage} method
* giving the implementing store way to do anything that it needs to do
* before the fileChannel is closed.
*
* @throws IOException
* If problem when invoking {@link #closeStorage()}
*/
public void close()
{
if ( fileChannel == null )
{
return;
}
closeStorage();
if ( windowPool != null )
{
windowPool.close();
windowPool = null;
}
if ( isReadOnly() && !isBackupSlave() )
{
try
{
fileChannel.close();
}
catch ( IOException e )
{
throw new UnderlyingStorageException( e );
}
return;
}
long highId = idGenerator.getHighId();
int recordSize = -1;
if ( this instanceof AbstractDynamicStore )
{
recordSize = ((AbstractDynamicStore) this).getBlockSize();
}
else if ( this instanceof AbstractStore )
{
recordSize = ((AbstractStore) this).getRecordSize();
}
closeIdGenerator();
boolean success = false;
IOException storedIoe = null;
// hack for WINBLOWS
if ( !readOnly || backupSlave )
{
for ( int i = 0; i < 10; i++ )
{
try
{
fileChannel.position( highId * recordSize );
ByteBuffer buffer = ByteBuffer.wrap(
getTypeAndVersionDescriptor().getBytes() );
fileChannel.write( buffer );
fileChannel.truncate( fileChannel.position() );
fileChannel.force( false );
fileLock.release();
fileChannel.close();
fileChannel = null;
success = true;
break;
}
catch ( IOException e )
{
storedIoe = e;
System.gc();
}
}
}
else
{
try
{
fileChannel.close();
}
catch ( IOException e )
{
e.printStackTrace();
}
}
if ( !success )
{
throw new UnderlyingStorageException( "Unable to close store "
+ getStorageFileName(), storedIoe );
}
}
/**
* Returns a <CODE>FileChannel</CODE> to this storage's file. If
* <CODE>close()</CODE> method has been invoked <CODE>null</CODE> will be
* returned.
*
* @return A file channel to this storage
*/
protected final FileChannel getFileChannel()
{
return fileChannel;
}
/**
* @return The highest possible id in use, -1 if no id in use.
*/
public long getHighestPossibleIdInUse()
{
return idGenerator.getHighId() - 1;
}
/**
* @return The total number of ids in use.
*/
public long getNumberOfIdsInUse()
{
return idGenerator.getNumberOfIdsInUse();
}
public WindowPoolStats getWindowPoolStats()
{
return windowPool.getStats();
}
}