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