/** * Copyright 2013 Oak Ridge National Laboratory * Author: James Horey <horeyjl@ornl.gov> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package gov.ornl.keva.sstable; /** * Java libs. **/ import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.io.RandomAccessFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.channels.FileChannel; import java.security.AccessController; import java.security.PrivilegedAction; import java.lang.reflect.Method; /** * Trove libs. **/ import gnu.trove.map.hash.TCustomHashMap; import gnu.trove.strategy.IdentityHashingStrategy; /** * Helper class used by sstables to allocate file buffers. The allocator * tries to use a memory-mapped buffer if possible, but otherwise will use * a normal file-based buffer. * * @author James Horey */ public class SSTableBufferAllocator { /** * Maximum size of memory mapped buffer (32MB). */ public static final int MAX_BUFFER_SIZE = 32 * 1024 * 1024; private static boolean mmapEnable = false; private static SSTableBufferAllocator instance; /** * Keep track of which buffers are used for which paths. */ private final TCustomHashMap<ByteBuffer, String> buffers = new TCustomHashMap<>(new IdentityHashingStrategy<ByteBuffer>()); /** * Keep track of which mapped buffers are used for which * open file channels. */ private final TCustomHashMap<ByteBuffer, FileChannel> channels = new TCustomHashMap<>(new IdentityHashingStrategy<ByteBuffer>()); static { // Static initializers mmapEnable = tryMMap(); instance = new SSTableBufferAllocator(); } /** * Private constructor to force singleton. **/ private SSTableBufferAllocator() { } /** * Get a new instance of the allocator. * * @return New allocator instance */ public static SSTableBufferAllocator newInstance() { return instance; } /** * Allocate a new buffer. */ protected synchronized ByteBuffer allocateBuffer(final String path, final long offset, final long size) { int bufferSize; if(size < Integer.MIN_VALUE || size > Integer.MAX_VALUE) { bufferSize = MAX_BUFFER_SIZE; } else { bufferSize = Math.min((int)size, MAX_BUFFER_SIZE); } ByteBuffer newBuffer; try { FileChannel fc = new RandomAccessFile(path, "rw").getChannel(); newBuffer = fc.map(FileChannel.MapMode.READ_WRITE, offset, bufferSize); channels.put(newBuffer, fc); // System.out.printf("renewing buffer size:%d offset:%d\n", size, offset); } catch(Exception e) { e.printStackTrace(); return null; } buffers.put(newBuffer, path); return newBuffer; } /** * Get another write buffer that is offset from the supplied. * * @param buffer The old byte buffer. The new byte buffer should be * mapped to the space after the old buffer. * @param size Maximum size of the new buffer. * @return A new mapped bytebuffer */ protected ByteBuffer renewWriteBuffer(final ByteBuffer buffer, final long offset, final long size) { // System.out.printf("closing buffer pos:%d capacity:%d\n", // buffer.position(), buffer.capacity()); // Get a new buffer. ByteBuffer newBuffer = allocateBuffer(buffers.get(buffer), offset, size); // Unmap the old buffer. flushBuffer(buffer); return newBuffer; } /** * Get a byte buffer opened for writing. * * @param path Path of where to locate the output file * @param size Maximum size of the buffer * @return Writeable byte buffer */ protected ByteBuffer getWriteBuffer(final String path, final long size) { return allocateBuffer(path, 0, size); } /** * Flush the buffer back to disk. * * @param buffer Writeable buffer to flush */ protected void flushBuffer(final ByteBuffer buffer) { unmap(buffer); // Unmap the buffer from memory. buffers.remove(buffer); // Remove from active list. } /** * Try unmapping the byte buffer. This is NOT a condoned method, but it * is the only way since Java does not support this operation. **/ private void unmap(final Object buffer) { AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { Method getCleanerMethod = buffer.getClass ().getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object [0]); cleaner.clean(); } catch(Exception e) { e.printStackTrace(); } return null; } }); // Close the file channel. FileChannel fc = channels.remove((ByteBuffer)buffer); try { fc.close(); } catch(IOException e) { e.printStackTrace(); } } /** * Try to open an mmap file in read-write mode. Some virtual machines * do not let us do this (I'm looking at you VirtualBox), so better to check. **/ private static boolean tryMMap() { try { // Create a small temporary file and open for writing. FileChannel fc = FileChannel.open(Paths.get("/tmp/test.dat"), StandardOpenOption.READ, StandardOpenOption.WRITE); // Try to map it. MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_WRITE, 0, 4); buffer.put((byte)'y'); // Write something. fc.close(); return true; } catch(IOException e) { // Nope didn't work. return false; } } }