/**
* Copyright (c) 2002-2012 "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.consistency.store.windowpool;
import static java.lang.String.format;
import static org.neo4j.kernel.impl.nioneo.store.WindowPoolStats.extractName;
import java.io.IOException;
import org.neo4j.kernel.impl.nioneo.store.OperationType;
import org.neo4j.kernel.impl.nioneo.store.PersistenceWindow;
import org.neo4j.kernel.impl.nioneo.store.UnderlyingStorageException;
import org.neo4j.kernel.impl.nioneo.store.WindowPoolStats;
import org.neo4j.consistency.store.paging.PageLoadFailureException;
import org.neo4j.consistency.store.paging.PageReplacementStrategy;
import org.neo4j.kernel.impl.nioneo.store.windowpool.WindowPool;
public class ScanResistantWindowPool implements WindowPool,
PageReplacementStrategy.Storage<PersistenceWindow, WindowPage>
{
private final String storeFileName;
private final FileMapper fileMapper;
private final PageReplacementStrategy replacementStrategy;
private final int bytesPerRecord;
private final int recordsPerPage;
private final int reportInterval;
private final MappingStatisticsListener statisticsListener;
private WindowPage[] pages = new WindowPage[0];
private int acquireCount = 0;
private int mapCount = 0;
public ScanResistantWindowPool( String storeFileName, int bytesPerRecord, int targetBytesPerPage,
FileMapper fileMapper, PageReplacementStrategy replacementStrategy,
int reportInterval, MappingStatisticsListener statisticsListener )
throws IOException
{
this.storeFileName = storeFileName;
this.bytesPerRecord = bytesPerRecord;
this.fileMapper = fileMapper;
this.replacementStrategy = replacementStrategy;
this.statisticsListener = statisticsListener;
this.recordsPerPage = calculateNumberOfRecordsPerPage( bytesPerRecord, targetBytesPerPage );
this.reportInterval = reportInterval;
this.setupPages();
}
private static int calculateNumberOfRecordsPerPage( int bytesPerRecord, int targetBytesPerPage )
{
if ( bytesPerRecord <= 0 || bytesPerRecord > targetBytesPerPage )
{
throw new IllegalArgumentException( format( "number of bytes per record [%d] " +
"is not in the valid range [1-%d]", bytesPerRecord, targetBytesPerPage ) );
}
return targetBytesPerPage / bytesPerRecord;
}
private void setupPages() throws IOException
{
// pre-allocate pages that exist already
page( fileMapper.fileSizeInBytes() / bytesPerRecord );
}
private int pageNumber( long position )
{
long pageNumber = position / recordsPerPage;
if ( pageNumber + 1 > Integer.MAX_VALUE )
{
throw new IllegalArgumentException( format( "Position [record %d] with current page size [%d records/page]"
+ " implies an impossible page number [%d].", position, recordsPerPage, pageNumber ) );
}
return (int) (position / recordsPerPage);
}
private WindowPage page( long position )
{
int pageNumber = pageNumber( position );
if ( pageNumber >= pages.length )
{
WindowPage[] newPages = new WindowPage[pageNumber + 1];
System.arraycopy( pages, 0, newPages, 0, pages.length );
for ( int i = pages.length; i < newPages.length; i++ )
{
newPages[i] = new WindowPage( i * (long) recordsPerPage );
}
pages = newPages;
}
return pages[pageNumber];
}
@Override
public PersistenceWindow acquire( long position, OperationType operationType )
{
if ( operationType != OperationType.READ )
{
throw new UnsupportedOperationException( "Only supports READ operations." );
}
try
{
acquireCount++;
return replacementStrategy.acquire( page( position ), this );
}
catch ( PageLoadFailureException e )
{
throw new UnderlyingStorageException( "Unable to load position["
+ position + "] @[" + position * bytesPerRecord + "]", e );
}
finally
{
reportStats();
}
}
private int lastMapCount;
private long lastReportTime = System.currentTimeMillis();
private void reportStats()
{
if ( acquireCount % reportInterval == 0 )
{
int deltaMapCount = mapCount - lastMapCount;
lastMapCount = mapCount;
long currentTime = System.currentTimeMillis();
long deltaTime = currentTime - lastReportTime;
lastReportTime = currentTime;
statisticsListener.onStatistics(
extractName( storeFileName ), reportInterval, deltaMapCount, deltaTime );
}
}
@Override
public void release( PersistenceWindow window )
{
// we're not using lockable windows, so no action required
}
@Override
public void flushAll()
{
// current implementation is read-only, so no need to flush
}
@Override
public void close()
{
for ( WindowPage page : pages )
{
replacementStrategy.forceEvict( page );
}
}
@Override
public WindowPoolStats getStats()
{
return new WindowPoolStats( storeFileName, 0, 0, pages.length,
bytesPerRecord * recordsPerPage, acquireCount - mapCount, mapCount, 0, 0, 0, 0, 0 );
}
@Override
public PersistenceWindow load( WindowPage page ) throws PageLoadFailureException
{
try
{
mapCount++;
return fileMapper.mapWindow( page.firstRecord, recordsPerPage, bytesPerRecord );
}
catch ( IOException e )
{
throw new PageLoadFailureException( e );
}
}
}