org.neo4j.io.pagecache.impl.SingleFilePageSwapper.java Source code

Java tutorial

Introduction

Here is the source code for org.neo4j.io.pagecache.impl.SingleFilePageSwapper.java

Source

/*
 * 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.impl;

import org.apache.commons.lang3.SystemUtils;
import sun.nio.ch.FileChannelImpl;

import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;

import org.neo4j.io.fs.FileSystemAbstraction;
import org.neo4j.io.fs.StoreChannel;
import org.neo4j.io.fs.StoreFileChannel;
import org.neo4j.io.fs.StoreFileChannelUnwrapper;
import org.neo4j.io.pagecache.Page;
import org.neo4j.io.pagecache.PageCursor;
import org.neo4j.io.pagecache.PageEvictionCallback;
import org.neo4j.io.pagecache.PageSwapper;
import org.neo4j.io.pagecache.impl.muninn.MuninnPageCache;
import org.neo4j.unsafe.impl.internal.dragons.UnsafeUtil;

import static java.lang.String.format;

/**
 * A simple PageSwapper implementation that directs all page swapping to a
 * single file on the file system.
 *
 * It additionally tracks the file size precisely, to avoid calling into the
 * file system whenever the size of the given file is queried.
 */
public class SingleFilePageSwapper implements PageSwapper {
    private static int defaultChannelStripePower() {
        int vcores = Runtime.getRuntime().availableProcessors();
        // Find the lowest 2's exponent that can accommodate 'vcores'
        int stripePower = 32 - Integer.numberOfLeadingZeros(vcores - 1);
        return Math.min(64, Math.max(1, stripePower));
    }

    // Exponent of 2 of how many channels we open per file:
    private static final int channelStripePower = Integer.getInteger(
            "org.neo4j.io.pagecache.implSingleFilePageSwapper.channelStripePower", defaultChannelStripePower());

    // Exponent of 2 of how many consecutive pages go to the same stripe
    private static final int channelStripeShift = Integer
            .getInteger("org.neo4j.io.pagecache.implSingleFilePageSwapper.channelStripeShift", 4);

    private static final int channelStripeCount = 1 << channelStripePower;
    private static final int channelStripeMask = channelStripeCount - 1;
    private static final int tokenChannelStripe = 0;
    private static final long tokenFilePageId = 0;

    private static final long fileSizeOffset = UnsafeUtil.getFieldOffset(SingleFilePageSwapper.class, "fileSize");

    private static final ThreadLocal<ByteBuffer> proxyCache = new ThreadLocal<>();
    private static final MethodHandle positionLockGetter = getPositionLockGetter();

    private static MethodHandle getPositionLockGetter() {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            Field field = FileChannelImpl.class.getDeclaredField("positionLock");
            field.setAccessible(true);
            return lookup.unreflectGetter(field);
        } catch (Exception e) {
            return null;
        }
    }

    private static ByteBuffer proxy(long buffer, int bufferLength) throws IOException {
        ByteBuffer buf = proxyCache.get();
        if (buf != null) {
            UnsafeUtil.initDirectByteBuffer(buf, buffer, bufferLength);
            return buf;
        }
        return createAndGetNewBuffer(buffer, bufferLength);
    }

    private static ByteBuffer createAndGetNewBuffer(long buffer, int bufferLength) throws IOException {
        ByteBuffer buf;
        try {
            buf = UnsafeUtil.newDirectByteBuffer(buffer, bufferLength);
        } catch (Exception e) {
            throw new IOException(e);
        }
        proxyCache.set(buf);
        return buf;
    }

    private final FileSystemAbstraction fs;
    private final File file;
    private final int filePageSize;
    private volatile PageEvictionCallback onEviction;
    private final StoreChannel[] channels;
    private FileLock fileLock;
    private final boolean hasPositionLock;

    // Guarded by synchronized(this). See tryReopen() and close().
    private boolean closed;

    @SuppressWarnings("unused") // Accessed through unsafe
    private volatile long fileSize;

    public SingleFilePageSwapper(File file, FileSystemAbstraction fs, int filePageSize,
            PageEvictionCallback onEviction) throws IOException {
        this.fs = fs;
        this.file = file;
        this.channels = new StoreChannel[channelStripeCount];
        for (int i = 0; i < channelStripeCount; i++) {
            channels[i] = fs.open(file, "rw");
        }
        this.filePageSize = filePageSize;
        this.onEviction = onEviction;
        increaseFileSizeTo(channels[tokenChannelStripe].size());

        try {
            acquireLock();
        } catch (IOException e) {
            closeAndCollectExceptions(0, e);
        }
        hasPositionLock = channels[0].getClass() == StoreFileChannel.class
                && StoreFileChannelUnwrapper.unwrap(channels[0]).getClass() == sun.nio.ch.FileChannelImpl.class;
    }

    private void increaseFileSizeTo(long newFileSize) {
        long currentFileSize;
        do {
            currentFileSize = getCurrentFileSize();
        } while (currentFileSize < newFileSize
                && !UnsafeUtil.compareAndSwapLong(this, fileSizeOffset, currentFileSize, newFileSize));
    }

    private long getCurrentFileSize() {
        return UnsafeUtil.getLongVolatile(this, fileSizeOffset);
    }

    private void setCurrentFileSize(long size) {
        UnsafeUtil.putLongVolatile(this, fileSizeOffset, size);
    }

    private void acquireLock() throws IOException {
        if (SystemUtils.IS_OS_WINDOWS) {
            // We don't take file locks on the individual store files on Windows, because once you've taking
            // a file lock on a channel, you can only do IO on that file through that channel. This would
            // mean that we can't stripe our FileChannels on Windows, which is the platform that needs striped
            // channels the most because of lack of pwrite and pread support.
            // This is generally fine, because the StoreLocker and the lock file will protect the store from
            // being opened by multiple instances at the same time anyway.
            return;
        }

        try {
            fileLock = channels[tokenChannelStripe].tryLock();
            if (fileLock == null) {
                throw new FileLockException(file);
            }
        } catch (OverlappingFileLockException e) {
            throw new FileLockException(file, e);
        }
    }

    private StoreChannel channel(long filePageId) {
        int stripe = stripe(filePageId);
        return channels[stripe];
    }

    private static int stripe(long filePageId) {
        return (int) (filePageId >>> channelStripeShift) & channelStripeMask;
    }

    private int swapIn(StoreChannel channel, Page page, long fileOffset, int filePageSize) throws IOException {
        int cachePageSize = page.size();
        long address = page.address();
        int readTotal = 0;
        try {
            ByteBuffer bufferProxy = proxy(address, filePageSize);
            int read;
            do {
                read = channel.read(bufferProxy, fileOffset + readTotal);
            } while (read != -1 && (readTotal += read) < filePageSize);

            // Zero-fill the rest.
            assert readTotal >= 0 && filePageSize <= cachePageSize && readTotal <= filePageSize : format(
                    "pointer = %h, readTotal = %s, length = %s, page size = %s", address, readTotal, filePageSize,
                    cachePageSize);
            UnsafeUtil.setMemory(address + readTotal, filePageSize - readTotal, MuninnPageCache.ZERO_BYTE);
            return readTotal;
        } catch (IOException e) {
            throw e;
        } catch (Throwable e) {
            String msg = format("Read failed after %s of %s bytes from fileOffset %s", readTotal, filePageSize,
                    fileOffset);
            throw new IOException(msg, e);
        }
    }

    private int swapOut(Page page, long fileOffset, StoreChannel channel) throws IOException {
        long address = page.address();
        try {
            ByteBuffer bufferProxy = proxy(address, filePageSize);
            channel.writeAll(bufferProxy, fileOffset);
        } catch (IOException e) {
            throw e;
        } catch (Throwable e) {
            throw new IOException(e);
        }
        return filePageSize;
    }

    private void clear(Page page) {
        UnsafeUtil.setMemory(page.address(), page.size(), MuninnPageCache.ZERO_BYTE);
    }

    @Override
    public long read(long filePageId, Page page) throws IOException {
        long fileOffset = pageIdToPosition(filePageId);
        try {
            if (fileOffset < getCurrentFileSize()) {
                return swapIn(channel(filePageId), page, fileOffset, filePageSize);
            } else {
                clear(page);
            }
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(filePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            long bytesRead = read(filePageId, page);
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
            return bytesRead;
        }
        return 0;
    }

    @Override
    public long read(long startFilePageId, Page[] pages, int arrayOffset, int length) throws IOException {
        if (positionLockGetter != null && hasPositionLock) {
            try {
                return readPositionedVectoredToFileChannel(startFilePageId, pages, arrayOffset, length);
            } catch (IOException ioe) {
                throw ioe;
            } catch (Exception ignore) {
                // There's a lot of reflection going on in that method. We ignore everything that can go wrong, and
                // isn't exactly an IOException. Instead, we'll try our fallback code and see what it says.
            }
        }
        return readPositionedVectoredFallback(startFilePageId, pages, arrayOffset, length);
    }

    private long readPositionedVectoredToFileChannel(long startFilePageId, Page[] pages, int arrayOffset,
            int length) throws Exception {
        long fileOffset = pageIdToPosition(startFilePageId);
        FileChannel channel = unwrappedChannel(startFilePageId);
        ByteBuffer[] srcs = convertToByteBuffers(pages, arrayOffset, length);
        long bytesRead = lockPositionReadVector(startFilePageId, channel, fileOffset, srcs);
        if (bytesRead == -1) {
            for (Page page : pages) {
                UnsafeUtil.setMemory(page.address(), filePageSize, MuninnPageCache.ZERO_BYTE);
            }
            return 0;
        } else if (bytesRead < filePageSize * length) {
            int pagesRead = (int) (bytesRead / filePageSize);
            int bytesReadIntoLastReadPage = (int) (bytesRead % filePageSize);
            int pagesNeedingZeroing = length - pagesRead;
            for (int i = 0; i < pagesNeedingZeroing; i++) {
                Page page = pages[arrayOffset + pagesRead + i];
                long bytesToZero = filePageSize;
                long address = page.address();
                if (i == 0) {
                    address += bytesReadIntoLastReadPage;
                    bytesToZero -= bytesReadIntoLastReadPage;
                }
                UnsafeUtil.setMemory(address, bytesToZero, MuninnPageCache.ZERO_BYTE);
            }
        }
        return bytesRead;
    }

    private long lockPositionReadVector(long filePageId, FileChannel channel, long fileOffset, ByteBuffer[] srcs)
            throws IOException {
        try {
            long toRead = filePageSize * (long) srcs.length;
            long read, readTotal = 0;
            synchronized (positionLock(channel)) {
                channel.position(fileOffset);
                do {
                    read = channel.read(srcs);
                } while (read != -1 && (readTotal += read) < toRead);
                return readTotal;
            }
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(filePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            channel = unwrappedChannel(filePageId);
            long bytesWritten = lockPositionReadVector(filePageId, channel, fileOffset, srcs);
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
            return bytesWritten;
        }
    }

    private int readPositionedVectoredFallback(long startFilePageId, Page[] pages, int arrayOffset, int length)
            throws IOException {
        int bytes = 0;
        for (int i = 0; i < length; i++) {
            bytes += read(startFilePageId + i, pages[arrayOffset + i]);
        }
        return bytes;
    }

    @Override
    public long write(long filePageId, Page page) throws IOException {
        long fileOffset = pageIdToPosition(filePageId);
        increaseFileSizeTo(fileOffset + filePageSize);
        try {
            StoreChannel channel = channel(filePageId);
            return swapOut(page, fileOffset, channel);
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(filePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            long bytesWritten = write(filePageId, page);
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
            return bytesWritten;
        }
    }

    @Override
    public long write(long startFilePageId, Page[] pages, int arrayOffset, int length) throws IOException {
        if (positionLockGetter != null && hasPositionLock) {
            try {
                return writePositionedVectoredToFileChannel(startFilePageId, pages, arrayOffset, length);
            } catch (IOException ioe) {
                throw ioe;
            } catch (Exception ignore) {
                // There's a lot of reflection going on in that method. We ignore everything that can go wrong, and
                // isn't exactly an IOException. Instead, we'll try our fallback code and see what it says.
            }
        }
        return writePositionVectoredFallback(startFilePageId, pages, arrayOffset, length);
    }

    private long writePositionedVectoredToFileChannel(long startFilePageId, Page[] pages, int arrayOffset,
            int length) throws Exception {
        long fileOffset = pageIdToPosition(startFilePageId);
        increaseFileSizeTo(fileOffset + (filePageSize * length));
        FileChannel channel = unwrappedChannel(startFilePageId);
        ByteBuffer[] srcs = convertToByteBuffers(pages, arrayOffset, length);
        return lockPositionWriteVector(startFilePageId, channel, fileOffset, srcs);
    }

    private ByteBuffer[] convertToByteBuffers(Page[] pages, int arrayOffset, int length) throws Exception {
        ByteBuffer[] buffers = new ByteBuffer[length];
        for (int i = 0; i < length; i++) {
            Page page = pages[arrayOffset + i];
            buffers[i] = UnsafeUtil.newDirectByteBuffer(page.address(), filePageSize);
        }
        return buffers;
    }

    private FileChannel unwrappedChannel(long startFilePageId) {
        StoreChannel storeChannel = channel(startFilePageId);
        return StoreFileChannelUnwrapper.unwrap(storeChannel);
    }

    private long lockPositionWriteVector(long filePageId, FileChannel channel, long fileOffset, ByteBuffer[] srcs)
            throws IOException {
        try {
            long toWrite = filePageSize * (long) srcs.length;
            long bytesWritten = 0;
            synchronized (positionLock(channel)) {
                channel.position(fileOffset);
                do {
                    bytesWritten += channel.write(srcs);
                } while (bytesWritten < toWrite);
                return bytesWritten;
            }
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(filePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            channel = unwrappedChannel(filePageId);
            long bytesWritten = lockPositionWriteVector(filePageId, channel, fileOffset, srcs);
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
            return bytesWritten;
        }
    }

    private Object positionLock(FileChannel channel) {
        sun.nio.ch.FileChannelImpl impl = (FileChannelImpl) channel;
        try {
            return (Object) positionLockGetter.invokeExact(impl);
        } catch (Throwable th) {
            throw new LinkageError("No getter for FileChannel.positionLock", th);
        }
    }

    private int writePositionVectoredFallback(long startFilePageId, Page[] pages, int arrayOffset, int length)
            throws IOException {
        int bytes = 0;
        for (int i = 0; i < length; i++) {
            bytes += write(startFilePageId + i, pages[arrayOffset + i]);
        }
        return bytes;
    }

    @Override
    public void evicted(long filePageId, Page page) {
        PageEvictionCallback callback = this.onEviction;
        if (callback != null) {
            callback.onEvict(filePageId, page);
        }
    }

    @Override
    public File file() {
        return file;
    }

    private long pageIdToPosition(long pageId) {
        return filePageSize * pageId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        SingleFilePageSwapper that = (SingleFilePageSwapper) o;

        return file.equals(that.file);

    }

    @Override
    public int hashCode() {
        return file.hashCode();
    }

    /**
     * Reopens the channel if it has been closed and the close() method on
     * this swapper has not been called. In other words, if the channel has
     * been "accidentally" closed by an interrupt or the like.
     *
     * If the channel has been explicitly closed with the PageSwapper#close()
     * method, then this method will re-throw the passed-in exception.
     *
     * If the reopening of the file fails with an exception for some reason,
     * then that exception is added as a suppressed exception to the passed in
     * ClosedChannelException, and the CCE is then rethrown.
     */
    private synchronized void tryReopen(long filePageId, ClosedChannelException closedException)
            throws ClosedChannelException {
        int stripe = stripe(filePageId);
        StoreChannel channel = channels[stripe];
        if (channel.isOpen()) {
            // Someone got ahead of us, presumably. Nothing to do.
            return;
        }

        if (closed) {
            // We've been explicitly closed, so we shouldn't reopen the
            // channel.
            throw closedException;
        }

        try {
            channels[stripe] = fs.open(file, "rw");
            if (stripe == tokenChannelStripe) {
                // The closing of a FileChannel also releases all associated file locks.
                acquireLock();
            }
        } catch (IOException e) {
            closedException.addSuppressed(e);
            throw closedException;
        }
    }

    @Override
    public synchronized void close() throws IOException {
        closed = true;
        try {
            closeAndCollectExceptions(0, null);
        } finally {
            // Eagerly relinquish our reference to the onEviction callback, because even though
            // we've closed the PagedFile at this point, there are likely still pages in the cache that are bound to
            // this swapper, and will stay bound, until the eviction threads eventually gets around to kicking them out.
            // It is especially important to null out the onEviction callback field, because it is in turn holding on to
            // the striped translation table, which can be a rather large structure.
            onEviction = null;
        }
    }

    private void closeAndCollectExceptions(int channelIndex, IOException exception) throws IOException {
        if (channelIndex == channels.length) {
            if (exception != null) {
                throw exception;
            }
            return;
        }

        try {
            channels[channelIndex].close();
        } catch (IOException e) {
            if (exception == null) {
                exception = e;
            } else {
                exception.addSuppressed(e);
            }
        }
        closeAndCollectExceptions(channelIndex + 1, exception);
    }

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

    @Override
    public void force() throws IOException {
        try {
            channel(tokenFilePageId).force(false);
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(tokenFilePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            force();
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public long getLastPageId() throws IOException {
        long channelSize = getCurrentFileSize();
        if (channelSize == 0) {
            return PageCursor.UNBOUND_PAGE_ID;
        }
        long div = channelSize / filePageSize;
        long mod = channelSize % filePageSize;
        return mod == 0 ? div - 1 : div;
    }

    @Override
    public void truncate() throws IOException {
        setCurrentFileSize(0);
        try {
            channel(tokenFilePageId).truncate(0);
        } catch (ClosedChannelException e) {
            // AsynchronousCloseException is a subclass of
            // ClosedChannelException, and ClosedByInterruptException is in
            // turn a subclass of AsynchronousCloseException.
            tryReopen(tokenFilePageId, e);
            boolean interrupted = Thread.interrupted();
            // Recurse because this is hopefully a very rare occurrence.
            truncate();
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public String toString() {
        return "SingleFilePageSwapper{" + "filePageSize=" + filePageSize + ", file=" + file + '}';
    }
}