/*
* 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.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.neo4j.kernel.impl.core.ReadOnlyDbException;
/**
* An abstract representation of a store. A store is a file that contains
* records. Each record has a fixed size (<CODE>getRecordSize()</CODE>) so
* the position for a record can be calculated by
* <CODE>id * getRecordSize()</CODE>.
* <p>
* A store has an {@link IdGenerator} managing the records that are free or in
* use.
*/
public abstract class AbstractStore extends CommonAbstractStore
{
/**
* Returnes the fixed size of each record in this store.
*
* @return The record size
*/
public abstract int getRecordSize();
/**
* Creates a new empty store. The factory method returning an implementation
* of some store type should make use of this method to initialize an empty
* store.
* <p>
* This method will create a empty store containing the descriptor returned
* by the <CODE>getTypeAndVersionDescriptor()</CODE>. The id generator
* used by this store will also be created
*
* @param fileName
* The file name of the store that will be created
* @param typeAndVersionDescriptor
* The type and version descriptor that identifies this store
* @throws IOException
* If fileName is null or if file exists
*/
protected static void createEmptyStore( String fileName,
String typeAndVersionDescriptor )
{
// sanity checks
if ( fileName == null )
{
throw new IllegalArgumentException( "Null filename" );
}
File file = new File( fileName );
if ( file.exists() )
{
throw new IllegalStateException( "Can't create store[" + fileName
+ "], file already exists" );
}
// write the header
try
{
FileChannel channel = new FileOutputStream( fileName ).getChannel();
int endHeaderSize = typeAndVersionDescriptor.getBytes().length;
ByteBuffer buffer = ByteBuffer.allocate( endHeaderSize );
buffer.put( typeAndVersionDescriptor.getBytes() ).flip();
channel.write( buffer );
channel.force( false );
channel.close();
}
catch ( IOException e )
{
throw new UnderlyingStorageException( "Unable to create store "
+ fileName, e );
}
IdGeneratorImpl.createGenerator( fileName + ".id" );
}
public AbstractStore( String fileName, Map<?,?> config )
{
super( fileName, config );
}
public AbstractStore( String fileName )
{
super( fileName );
}
protected void loadStorage()
{
try
{
long fileSize = getFileChannel().size();
String expectedVersion = getTypeAndVersionDescriptor();
byte version[] = new byte[expectedVersion.getBytes().length];
ByteBuffer buffer = ByteBuffer.wrap( version );
if ( fileSize >= version.length )
{
getFileChannel().position( fileSize - version.length );
}
else if ( !isReadOnly() )
{
setStoreNotOk();
}
getFileChannel().read( buffer );
if ( !expectedVersion.equals( new String( version ) ) && !isReadOnly() )
{
if ( !versionFound( new String( version ) ) )
{
setStoreNotOk();
}
}
if ( getRecordSize() != 0
&& (fileSize - version.length) % getRecordSize() != 0 && !isReadOnly() )
{
setStoreNotOk();
}
if ( getStoreOk() && !isReadOnly() )
{
getFileChannel().truncate( fileSize - version.length );
}
}
catch ( IOException e )
{
throw new UnderlyingStorageException( "Unable to load store "
+ getStorageFileName(), e );
}
try
{
if ( !isReadOnly() || isBackupSlave() )
{
openIdGenerator();
}
else
{
openReadOnlyIdGenerator( getRecordSize() );
}
}
catch ( InvalidIdGeneratorException e )
{
setStoreNotOk();
}
setWindowPool( new PersistenceWindowPool( getStorageFileName(),
getRecordSize(), getFileChannel(), getMappedMem(),
getIfMemoryMapped(), isReadOnly() && !isBackupSlave() ) );
}
/**
* Returns the highest id in use by this store.
*
* @return The highest id in use
*/
public long getHighId()
{
return super.getHighId();
}
/**
* Sets the high id of {@link IdGenerator}.
*
* @param id
* The high id
*/
public void setHighId( int id )
{
super.setHighId( id );
}
protected void updateHighId()
{
try
{
long highId = getFileChannel().size() / getRecordSize();
setHighId( highId );
}
catch ( IOException e )
{
throw new UnderlyingStorageException( e );
}
}
private int findHighIdBackwards() throws IOException
{
FileChannel fileChannel = getFileChannel();
int recordSize = getRecordSize();
long fileSize = fileChannel.size();
long highId = fileSize / recordSize;
ByteBuffer byteBuffer = ByteBuffer.allocate( 1 );
for ( long i = highId; i > 0; i-- )
{
fileChannel.position( i * recordSize );
if ( fileChannel.read( byteBuffer ) > 0 )
{
byteBuffer.flip();
byte inUse = byteBuffer.get();
byteBuffer.clear();
if ( inUse != 0 )
{
return (int) i;
}
}
}
return 0;
}
/**
* Rebuilds the {@link IdGenerator} by looping through all records and
* checking if record in use or not.
*
* @throws IOException
* if unable to rebuild the id generator
*/
protected void rebuildIdGenerator()
{
if ( isReadOnly() && !isBackupSlave() )
{
throw new ReadOnlyDbException();
}
// TODO: fix this hardcoding
final byte RECORD_NOT_IN_USE = 0;
logger.fine( "Rebuilding id generator for[" + getStorageFileName()
+ "] ..." );
closeIdGenerator();
File file = new File( getStorageFileName() + ".id" );
if ( file.exists() )
{
boolean success = file.delete();
assert success;
}
IdGeneratorImpl.createGenerator( getStorageFileName() + ".id" );
openIdGenerator();
FileChannel fileChannel = getFileChannel();
long highId = 1;
long defraggedCount = 0;
try
{
long fileSize = fileChannel.size();
int recordSize = getRecordSize();
boolean fullRebuild = true;
if ( getConfig() != null )
{
String mode = (String)
getConfig().get( "rebuild_idgenerators_fast" );
if ( mode != null && mode.toLowerCase().equals( "true" ) )
{
fullRebuild = false;
highId = findHighIdBackwards();
}
}
ByteBuffer byteBuffer = ByteBuffer.wrap( new byte[1] );
LinkedList<Integer> freeIdList = new LinkedList<Integer>();
if ( fullRebuild )
{
for ( long i = 0; i * recordSize < fileSize && recordSize > 0;
i++ )
{
fileChannel.position( i * recordSize );
fileChannel.read( byteBuffer );
byteBuffer.flip();
byte inUse = byteBuffer.get();
byteBuffer.flip();
nextId();
if ( inUse == RECORD_NOT_IN_USE )
{
freeIdList.add( (int) i );
}
else
{
highId = i;
while ( !freeIdList.isEmpty() )
{
freeId( freeIdList.removeFirst() );
defraggedCount++;
}
}
}
}
}
catch ( IOException e )
{
throw new UnderlyingStorageException(
"Unable to rebuild id generator " + getStorageFileName(), e );
}
setHighId( highId + 1 );
logger.fine( "[" + getStorageFileName() + "] high id=" + getHighId()
+ " (defragged=" + defraggedCount + ")" );
closeIdGenerator();
openIdGenerator();
}
public abstract List<WindowPoolStats> getAllWindowPoolStats();
}