Skip to content

Commit

Permalink
Support open options DELETE_ON_CLOSE and EXCLUSIVE for PageCache.map()
Browse files Browse the repository at this point in the history
The `StandardOpenOption.DELETE_ON_CLOSE` will be useful for migrations.
Specifically, it has a use when migrating a store to a block device based storage system.
The `PageCacheOpenOption.EXCLUSIVE` may find use in the counts store, and generally in places where we want to ensure a single point of access to a file.
  • Loading branch information
chrisvest committed Feb 22, 2016
1 parent 51cba56 commit e9cdaa5
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 80 deletions.
20 changes: 13 additions & 7 deletions community/io/src/main/java/org/neo4j/io/pagecache/PageCache.java
Expand Up @@ -22,6 +22,7 @@
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.OpenOption; import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;


/** /**
* A page caching mechanism that allows caching multiple files and accessing their data * A page caching mechanism that allows caching multiple files and accessing their data
Expand All @@ -43,15 +44,20 @@ public interface PageCache extends AutoCloseable
* @param file The file to map. * @param file The file to map.
* @param pageSize The file page size to use for this mapping. If the file is already mapped with a different page * @param pageSize The file page size to use for this mapping. If the file is already mapped with a different page
* size, an exception will be thrown. * size, an exception will be thrown.
* @param openOptions The set of open options to use for mapping this file. The * @param openOptions The set of open options to use for mapping this file.
* {@link java.nio.file.StandardOpenOption#READ} and {@link java.nio.file.StandardOpenOption#WRITE} options always * The {@link StandardOpenOption#READ} and {@link StandardOpenOption#WRITE} options always implicitly specified.
* implicitly specified. The {@link java.nio.file.StandardOpenOption#CREATE} open option will create the given * The {@link StandardOpenOption#CREATE} open option will create the given file if it does not already exist, and
* file if it does not already exist, and the {@link java.nio.file.StandardOpenOption#TRUNCATE_EXISTING} will * the {@link StandardOpenOption#TRUNCATE_EXISTING} will truncate any existing file <em>iff</em> it has not already
* truncate any existing file <em>iff</em> it has not already been mapped. * been mapped.
* The {@link StandardOpenOption#DELETE_ON_CLOSE} will cause the file to be deleted after the last unmapping.
* The {@link PageCacheOpenOptions#EXCLUSIVE} will cause the {@code map} method to throw if the file is already
* mapped. Otherwise, the file will be mapped exclusively, and subsequent attempts at mapping the file will fail
* with an exception until the exclusively mapped file is closed.
* All other options are either silently ignored, or will cause an exception to be thrown. * All other options are either silently ignored, or will cause an exception to be thrown.
* @throws java.nio.file.NoSuchFileException if the given file does not exist, and the * @throws java.nio.file.NoSuchFileException if the given file does not exist, and the
* {@link java.nio.file.StandardOpenOption#CREATE} option was not specified. * {@link StandardOpenOption#CREATE} option was not specified.
* @throws IOException if the file could otherwise not be mapped. * @throws IOException if the file could otherwise not be mapped. Causes include the file being locked, or exclusive
* mapping conflicts.
*/ */
PagedFile map( File file, int pageSize, OpenOption... openOptions ) throws IOException; PagedFile map( File file, int pageSize, OpenOption... openOptions ) throws IOException;


Expand Down
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2002-2016 "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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.io.pagecache;

import java.io.File;
import java.nio.file.OpenOption;

/**
* {@link OpenOption}s that are specific to {@link PageCache#map(File, int, OpenOption...)},
* and not normally supported by file systems.
*/
public enum PageCacheOpenOptions implements OpenOption
{
/**
* Only allow a single mapping of the given file.
*/
EXCLUSIVE
}
Expand Up @@ -106,11 +106,17 @@ public interface PageSwapper
File file(); File file();


/** /**
* Close and release all resources associated with the file underlying this * Close and release all resources associated with the file underlying this PageSwapper.
* PageSwapper.
*/ */
void close() throws IOException; void close() throws IOException;


/**
* Close and release all resources associated with the file underlying this PageSwapper, and then delete that file.
* @throws IOException If an {@link IOException} occurs during either the closing or the deleting of the file. This
* may leave the file on the file system.
*/
void closeAndDelete() throws IOException;

/** /**
* Forces all writes done by this PageSwapper to the underlying storage device, such that the writes are durable * Forces all writes done by this PageSwapper to the underlying storage device, such that the writes are durable
* when this call returns. * when this call returns.
Expand Down
Expand Up @@ -694,6 +694,13 @@ private void closeAndCollectExceptions( int channelIndex, IOException exception
closeAndCollectExceptions( channelIndex + 1, exception ); closeAndCollectExceptions( channelIndex + 1, exception );
} }


@Override
public synchronized void closeAndDelete() throws IOException
{
close();
fs.deleteFile( file );
}

@Override @Override
public void force() throws IOException public void force() throws IOException
{ {
Expand Down
Expand Up @@ -32,6 +32,7 @@
import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.LockSupport;


import org.neo4j.io.pagecache.PageCache; import org.neo4j.io.pagecache.PageCache;
import org.neo4j.io.pagecache.PageCacheOpenOptions;
import org.neo4j.io.pagecache.PageSwapperFactory; import org.neo4j.io.pagecache.PageSwapperFactory;
import org.neo4j.io.pagecache.PagedFile; import org.neo4j.io.pagecache.PagedFile;
import org.neo4j.io.pagecache.tracing.EvictionEvent; import org.neo4j.io.pagecache.tracing.EvictionEvent;
Expand Down Expand Up @@ -308,6 +309,8 @@ public synchronized PagedFile map( File file, int filePageSize, OpenOption... op
} }
boolean createIfNotExists = false; boolean createIfNotExists = false;
boolean truncateExisting = false; boolean truncateExisting = false;
boolean deleteOnClose = false;
boolean exclusiveMapping = false;
for ( OpenOption option : openOptions ) for ( OpenOption option : openOptions )
{ {
if ( option.equals( StandardOpenOption.CREATE ) ) if ( option.equals( StandardOpenOption.CREATE ) )
Expand All @@ -318,6 +321,14 @@ else if ( option.equals( StandardOpenOption.TRUNCATE_EXISTING ) )
{ {
truncateExisting = true; truncateExisting = true;
} }
else if ( option.equals( StandardOpenOption.DELETE_ON_CLOSE ) )
{
deleteOnClose = true;
}
else if ( option.equals( PageCacheOpenOptions.EXCLUSIVE ) )
{
exclusiveMapping = true;
}
else if ( !ignoredOpenOptions.contains( option ) ) else if ( !ignoredOpenOptions.contains( option ) )
{ {
throw new UnsupportedOperationException( "Unsupported OpenOption: " + option ); throw new UnsupportedOperationException( "Unsupported OpenOption: " + option );
Expand Down Expand Up @@ -345,7 +356,21 @@ else if ( !ignoredOpenOptions.contains( option ) )
{ {
throw new UnsupportedOperationException( "Cannot truncate a file that is already mapped" ); throw new UnsupportedOperationException( "Cannot truncate a file that is already mapped" );
} }
if ( exclusiveMapping || pagedFile.isExclusiveMapping() )
{
String msg;
if ( exclusiveMapping )
{
msg = "Cannot exclusively map file because it is already mapped: " + file;
}
else
{
msg = "Cannot map file because it is already exclusively mapped: " + file;
}
throw new IOException( msg );
}
pagedFile.incrementRefCount(); pagedFile.incrementRefCount();
pagedFile.markDeleteOnClose( deleteOnClose );
return pagedFile; return pagedFile;
} }
current = current.next; current = current.next;
Expand All @@ -359,8 +384,10 @@ else if ( !ignoredOpenOptions.contains( option ) )
swapperFactory, swapperFactory,
tracer, tracer,
createIfNotExists, createIfNotExists,
truncateExisting ); truncateExisting,
exclusiveMapping );
pagedFile.incrementRefCount(); pagedFile.incrementRefCount();
pagedFile.markDeleteOnClose( deleteOnClose );
current = new FileMapping( file, pagedFile ); current = new FileMapping( file, pagedFile );
current.next = mappedFiles; current.next = mappedFiles;
mappedFiles = current; mappedFiles = current;
Expand Down
Expand Up @@ -60,6 +60,10 @@ final class MuninnPagedFile implements PagedFile


final PageSwapper swapper; final PageSwapper swapper;
private final CursorPool cursorPool; private final CursorPool cursorPool;
private final boolean exclusiveMapping;

// Guarded by the monitor lock on MuninnPageCache (map and unmap)
private boolean deleteOnClose;


/** /**
* The header state includes both the reference count of the PagedFile – 15 bits – and the ID of the last page in * The header state includes both the reference count of the PagedFile – 15 bits – and the ID of the last page in
Expand All @@ -85,12 +89,14 @@ final class MuninnPagedFile implements PagedFile
PageSwapperFactory swapperFactory, PageSwapperFactory swapperFactory,
PageCacheTracer tracer, PageCacheTracer tracer,
boolean createIfNotExists, boolean createIfNotExists,
boolean truncateExisting ) throws IOException boolean truncateExisting,
boolean exclusiveMapping ) throws IOException
{ {
this.pageCache = pageCache; this.pageCache = pageCache;
this.filePageSize = filePageSize; this.filePageSize = filePageSize;
this.cursorPool = new CursorPool( this ); this.cursorPool = new CursorPool( this );
this.tracer = tracer; this.tracer = tracer;
this.exclusiveMapping = exclusiveMapping;


// The translation table is an array of arrays of references to either null, MuninnPage objects, or Latch // The translation table is an array of arrays of references to either null, MuninnPage objects, or Latch
// objects. The table only grows the outer array, and all the inner "chunks" all stay the same size. This // objects. The table only grows the outer array, and all the inner "chunks" all stay the same size. This
Expand Down Expand Up @@ -175,7 +181,14 @@ public void close() throws IOException


void closeSwapper() throws IOException void closeSwapper() throws IOException
{ {
swapper.close(); if ( !deleteOnClose )
{
swapper.close();
}
else
{
swapper.closeAndDelete();
}
} }


@Override @Override
Expand Down Expand Up @@ -381,6 +394,11 @@ void increaseLastPageIdTo( long newLastPageId )
&& !UnsafeUtil.compareAndSwapLong( this, headerStateOffset, current, update ) ); && !UnsafeUtil.compareAndSwapLong( this, headerStateOffset, current, update ) );
} }


boolean isExclusiveMapping()
{
return exclusiveMapping;
}

/** /**
* Atomically increment the reference count for this mapped file. * Atomically increment the reference count for this mapped file.
*/ */
Expand Down Expand Up @@ -433,6 +451,11 @@ int getRefCount()
return (int) refCountOf( getHeaderState() ); return (int) refCountOf( getHeaderState() );
} }


void markDeleteOnClose( boolean deleteOnClose )
{
this.deleteOnClose |= deleteOnClose;
}

/** /**
* Grab a free page for the purpose of page faulting. Possibly blocking if * Grab a free page for the purpose of page faulting. Possibly blocking if
* none are immediately available. * none are immediately available.
Expand Down
Expand Up @@ -76,6 +76,12 @@ public void truncate() throws IOException
delegate.truncate(); delegate.truncate();
} }


@Override
public void closeAndDelete() throws IOException
{
delegate.closeAndDelete();
}

public long read( long startFilePageId, Page[] pages, int arrayOffset, int length ) throws IOException public long read( long startFilePageId, Page[] pages, int arrayOffset, int length ) throws IOException
{ {
return delegate.read( startFilePageId, pages, arrayOffset, length ); return delegate.read( startFilePageId, pages, arrayOffset, length );
Expand Down
Expand Up @@ -1599,6 +1599,7 @@ public void mustNotLiveLockIfWeRunOutOfEvictablePages() throws Exception
{ {
try try
{ {
//noinspection InfiniteLoopStatement
for ( long i = 0;; i++ ) for ( long i = 0;; i++ )
{ {
PageCursor cursor = pf.io( i, PF_SHARED_WRITE_LOCK ); PageCursor cursor = pf.io( i, PF_SHARED_WRITE_LOCK );
Expand Down Expand Up @@ -2275,7 +2276,7 @@ public void cursorCanReadUnsignedIntGreaterThanMaxInt() throws IOException
} }


@Test( timeout = SHORT_TIMEOUT_MILLIS, expected = IllegalStateException.class ) @Test( timeout = SHORT_TIMEOUT_MILLIS, expected = IllegalStateException.class )
public void pageCacheCloseMustThrowIfFilesAreStillMapped() throws IOException public void closeOnPageCacheMustThrowIfFilesAreStillMapped() throws IOException
{ {
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL ); getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );


Expand Down Expand Up @@ -3482,7 +3483,6 @@ public void mustThrowOnUnsupportedOpenOptions() throws Exception
{ {
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL ); getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
verifyMappingWithOpenOptionThrows( StandardOpenOption.CREATE_NEW ); verifyMappingWithOpenOptionThrows( StandardOpenOption.CREATE_NEW );
verifyMappingWithOpenOptionThrows( StandardOpenOption.DELETE_ON_CLOSE );
verifyMappingWithOpenOptionThrows( StandardOpenOption.SYNC ); verifyMappingWithOpenOptionThrows( StandardOpenOption.SYNC );
verifyMappingWithOpenOptionThrows( StandardOpenOption.DSYNC ); verifyMappingWithOpenOptionThrows( StandardOpenOption.DSYNC );
verifyMappingWithOpenOptionThrows( new OpenOption() verifyMappingWithOpenOptionThrows( new OpenOption()
Expand Down Expand Up @@ -3547,4 +3547,57 @@ public void mustThrowIfFileIsClosedMoreThanItIsMapped() throws Exception
pf.close(); pf.close();
pf.close(); pf.close();
} }

@Test( expected = NoSuchFileException.class )
public void fileMappedWithDeleteOnCloseMustNotExistAfterUnmap() throws Exception
{
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
pageCache.map( file( "a" ), filePageSize, StandardOpenOption.DELETE_ON_CLOSE ).close();
pageCache.map( file( "a" ), filePageSize );
}

@Test( expected = NoSuchFileException.class )
public void fileMappedWithDeleteOnCloseMustNotExistAfterLastUnmap() throws Exception
{
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
File file = file( "a" );
try ( PagedFile ignore = pageCache.map( file, filePageSize ) )
{
pageCache.map( file, filePageSize, StandardOpenOption.DELETE_ON_CLOSE ).close();
}
pageCache.map( file, filePageSize );
}

@Test
public void mustAllowMappingFileWithExclusiveOpenOption() throws Exception
{
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
try ( PagedFile pf = pageCache.map( file( "a" ), filePageSize, PageCacheOpenOptions.EXCLUSIVE );
PageCursor cursor = pf.io( 0, PF_SHARED_WRITE_LOCK ))
{
assertTrue( cursor.next() );
}
}

@Test( expected = IOException.class )
public void mustThrowWhenMappingFileAlreadyMappedWithExclusive() throws Exception
{
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
try ( PagedFile ignore = pageCache.map( file( "a" ), filePageSize, PageCacheOpenOptions.EXCLUSIVE ) )
{
pageCache.map( file( "a" ), filePageSize );
fail( "mapping should have thrown" );
}
}

@Test( expected = IOException.class )
public void mustThrowWhenExclusivelyMappingAlreadyMappedFile() throws Exception
{
getPageCache( fs, maxPages, pageCachePageSize, PageCacheTracer.NULL );
try ( PagedFile ignore = pageCache.map( file( "a" ), filePageSize ) )
{
pageCache.map( file( "a" ), filePageSize, PageCacheOpenOptions.EXCLUSIVE );
fail( "mapping should have thrown" );
}
}
} }

0 comments on commit e9cdaa5

Please sign in to comment.