/* * 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 com.github.ggrandes.kvstore.io; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Arrays; import java.util.Comparator; import com.github.ggrandes.kvstore.structures.hash.IntHashMap; import com.github.ggrandes.kvstore.utils.Check64bitsJVM; import org.apache.log4j.Logger; import com.github.ggrandes.kvstore.pool.BufferStacker; /** * File based Storage of fixed size blocks * This class is NOT Thread-Safe * * @author Guillermo Grandes / guillermo.grandes[at]gmail.com */ public class FileBlockStore { private static final Logger log = Logger.getLogger(FileBlockStore.class); /** * Size of block */ public final int blockSize; /** * File associated to this store */ private File file = null; /** * RamdomAccessFile for this store */ private RandomAccessFile raf = null; /** * FileChannel for this store */ private FileChannel fileChannel = null; /** * Support for mmap */ private boolean useMmap = false; /** * Support for Locking */ private boolean useLock = false; /** * ByteBuffer pool */ private final BufferStacker bufstack; /** * In Valid State? */ private boolean validState = false; /** * Callback called when flush buffers to disk */ private CallbackSync callback = null; /** * File Lock */ private FileLock lock = null; /** * Instantiate FileBlockStore * @param file name of file to open * @param blockSize size of block * @param isDirect use DirectByteBuffer or HeapByteBuffer? */ public FileBlockStore(final String file, final int blockSize, final boolean isDirect) { this(new File(file), blockSize, isDirect); } /** * Instantiate FileBlockStore * @param file file to open * @param blockSize size of block * @param isDirect use DirectByteBuffer or HeapByteBuffer? */ public FileBlockStore(final File file, final int blockSize, final boolean isDirect) { this.file = file; this.blockSize = blockSize; this.bufstack = BufferStacker.getInstance(blockSize, isDirect); } // ========= Open / Close ========= /** * Open file for read/write * @return true if valid state */ public boolean open() { return open(false); } /** * Open file * @param readOnly open for readOnly? * @return true if valid state */ public boolean open(final boolean readOnly) { if (isOpen()) { close(); } if (log.isDebugEnabled()) log.debug("open("+file+")"); try { raf = new RandomAccessFile(file, readOnly ? "r" : "rw"); fileChannel = raf.getChannel(); if (useLock) lock(readOnly); } catch(Exception e) { log.error("Exception in open()", e); try { unlock(); } catch(Exception ign) {} try { fileChannel.close(); } catch(Exception ign) {} try { raf.close(); } catch(Exception ign) {} raf = null; fileChannel = null; } validState = isOpen(); return validState; } /** * Close file */ public void close() { mmaps.clear(false); try { unlock(); } catch(Exception ign) {} try { fileChannel.close(); } catch(Exception ign) {} try { raf.close(); } catch(Exception ign) {} fileChannel = null; raf = null; validState = false; } // ========= Locking ====== /** * Lock file * @throws IOException */ public boolean lock(final boolean readOnly) throws IOException { if (isOpen() && lock == null) { lock = fileChannel.lock(0L, Long.MAX_VALUE, readOnly); return true; } return false; } /** * Unlock file * @throws IOException */ public boolean unlock() throws IOException { if (lock != null) { lock.release(); lock = null; return true; } return false; } // ========= Info ========= /** * @return size of block */ public int getBlockSize() { return blockSize; } /** * @return true if file is open */ public boolean isOpen() { try { if (fileChannel != null) return fileChannel.isOpen(); } catch(Exception ign) {} return false; } /** * @return size of file in blocks * @see #getBlockSize() */ public int sizeInBlocks() { try { final long len = file.length(); final long num_blocks = ((len / blockSize) + (((len % blockSize) == 0) ? 0 : 1)); if (log.isDebugEnabled()) log.debug("size()=" + num_blocks); return (int) num_blocks; } catch(Exception e) { log.error("Exception in sizeInBlocks()", e); } return -1; } // ========= Destroy ========= /** * Truncate file */ public void clear() { if (!validState) throw new InvalidStateException(); try { fileChannel.position(0).truncate(0); sync(); } catch(Exception e) { log.error("Exception in clear()", e); } } /** * Delete file */ public void delete() { close(); try { file.delete(); } catch(Exception ign) {} } // ========= Operations ========= /** * set callback called when buffers where synched to disk * @param callback */ public void setCallback(final CallbackSync callback) { this.callback = callback; } /** * Read block from file * @param index of block * @return ByteBuffer from pool with data */ public ByteBuffer get(final int index) { if (!validState) throw new InvalidStateException(); if (log.isDebugEnabled()) log.debug("get("+index+")"); try { if (useMmap) { final MappedByteBuffer mbb = getMmapForIndex(index); if (mbb != null) { return mbb; } // Fallback to RAF } final ByteBuffer buf = bufstack.pop(); fileChannel.position(index * blockSize).read(buf); buf.rewind(); return buf; } catch(Exception e) { log.error("Exception in get("+index+")", e); } return null; } /** * Write from buf to file * @param index of block * @param buf ByteBuffer to write * @return true if write is OK */ public boolean set(final int index, final ByteBuffer buf) { if (!validState) throw new InvalidStateException(); if (log.isDebugEnabled()) log.debug("set("+index+","+buf+")"); try { if (buf.limit() > blockSize) { log.error("ERROR: buffer.capacity="+buf.limit()+" > blocksize=" + blockSize); } if (useMmap) { final MappedByteBuffer mbb = getMmapForIndex(index); if (mbb != null) { mbb.put(buf); return true; } // Fallback to RAF } fileChannel.position(index * blockSize).write(buf); return true; } catch(Exception e) { log.error("Exception in set("+index+")", e); } return false; } /** * Alloc a WriteBuffer * @param index of block * @return WriteBuffer */ public WriteBuffer set(final int index) { if (useMmap) { final ByteBuffer buf = getMmapForIndex(index); if (buf != null) { return new WriteBuffer(this, index, useMmap, buf); } } return new WriteBuffer(this, index, false, bufstack.pop()); } /** * Release Read ByteBuffer * @param buf readed ByteBuffer */ public void release(final ByteBuffer buf) { if (!useMmap) bufstack.push(buf); } /** * Forces any updates to this file to be written to the storage device that contains it. */ public void sync() { if (!validState) throw new InvalidStateException(); if (useMmap) { syncAllMmaps(); } if (fileChannel != null) { try { fileChannel.force(false); } catch(Exception ign) {} } if (callback != null) callback.synched(); } public static interface CallbackSync { public void synched(); } public static class WriteBuffer { private final FileBlockStore storage; private final int index; private final boolean mmaped; private ByteBuffer buf; private WriteBuffer(final FileBlockStore storage, final int index, final boolean mmaped, final ByteBuffer buf) { this.storage = storage; this.index = index; this.mmaped = mmaped; this.buf = buf; } public ByteBuffer buf() { return buf; } /** * Save and release the buffer * @return successful operation? */ public boolean save() { if (mmaped) return true; final boolean ret = storage.set(index, buf); storage.release(buf); buf = null; return ret; } } // ========= Mmap =============== private static final boolean useSegments = true; private static final int segmentSize = (32 * 4096); // N_PAGES * PAGE=4KB // 128KB @SuppressWarnings("rawtypes") private final IntHashMap<BufferReference> mmaps = new IntHashMap<BufferReference>(128, BufferReference.class); /** * Comparator for write by Idx */ private Comparator<BufferReference<MappedByteBuffer>> comparatorByIdx = new Comparator<BufferReference<MappedByteBuffer>>() { @Override public int compare(final BufferReference<MappedByteBuffer> o1, final BufferReference<MappedByteBuffer> o2) { if (o1 == null) { if (o2 == null) return 0; // o1 == null & o2 == null return 1; // o1 == null & o2 != null } if (o2 == null) return -1; // o1 != null & o2 == null final int thisVal = (o1.idx < 0 ? -o1.idx : o1.idx); final int anotherVal = (o2.idx < 0 ? -o2.idx : o2.idx); return ((thisVal<anotherVal) ? -1 : ((thisVal==anotherVal) ? 0 : 1)); } }; /** * Is enabled mmap for this store? * @return true/false */ public boolean useMmap() { return useMmap; } /** * Enable mmap of files (default is not enabled), call before use {@link #open()} * <p/> * Recommended use of: {@link #enableMmapIfSupported()} * <p/> * <b>NOTE:</b> 32bit JVM can only address 2GB of memory, enable mmap can throw <b>java.lang.OutOfMemoryError: Map failed</b> exceptions */ public void enableMmap() { if (validState) throw new InvalidStateException(); if (Check64bitsJVM.JVMis64bits()) { log.info("Enabled mmap on 64bits JVM"); } else { log.warn("Enabled mmap on 32bits JVM, risk of: java.lang.OutOfMemoryError: Map failed"); } useMmap = true; } /** * Enable mmap of files (default is not enabled) if JVM is 64bits, call before use {@link #open()} */ public void enableMmapIfSupported() { if (validState) throw new InvalidStateException(); useMmap = Check64bitsJVM.JVMis64bits(); if (useMmap) { log.info("Enabled mmap on 64bits JVM"); } else { log.info("Disabled mmap on 32bits JVM"); } } /** * Enable Lock of files (default is not enabled), call before use {@link #open()} */ public void enableLocking() { if (validState) throw new InvalidStateException(); if (Boolean.getBoolean(Constants.PROP_IO_LOCKING)) { useLock = false; log.info("Disabled Locking in System Property (" + Constants.PROP_IO_LOCKING + ")"); } else { useLock = true; log.info("Enabled Locking"); } } private final int addressIndexToSegment(final int index) { return (int)(((long)index * blockSize) / segmentSize); } private final int addressIndexToSegmentOffset(final int index) { return (index % (segmentSize / blockSize)); } public final MappedByteBuffer getMmapForIndex(final int index) { if (!validState) throw new InvalidStateException(); final int mapIdx = (useSegments ? addressIndexToSegment(index) : index); final int mapSize = (useSegments ? segmentSize : blockSize); try { @SuppressWarnings("unchecked") final Reference<MappedByteBuffer> bref = mmaps.get(mapIdx); MappedByteBuffer mbb = null; if (bref != null) { mbb = bref.get(); } if (mbb == null) { // Create mmap final long mapOffset = ((long)mapIdx * mapSize); mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, mapOffset, mapSize); //mbb.load(); mmaps.put(mapIdx, new BufferReference<MappedByteBuffer>(mapIdx, mbb)); //log.info("Mapped index=" + index + " to offset=" + mapOffset + " size=" + mapSize); } else { mbb.clear(); } if (useSegments) { // slice segment final int sliceBegin = (addressIndexToSegmentOffset(index) * blockSize); final int sliceEnd = (sliceBegin + blockSize); //log.info("Mapping segment=" + mapIdx + " index=" + index + " sliceBegin=" + sliceBegin + " sliceEnd=" + sliceEnd); mbb.limit(sliceEnd); mbb.position(sliceBegin); mbb = (MappedByteBuffer) mbb.slice(); } return mbb; } catch (IOException e) { log.error("IOException in getMmapForIndex("+index+")", e); } return null; } private void syncAllMmaps() { @SuppressWarnings("unchecked") final BufferReference<MappedByteBuffer>[] maps = mmaps.getValues(); Arrays.sort(maps, comparatorByIdx); for (final Reference<MappedByteBuffer> ref : maps) { if (ref == null) break; final MappedByteBuffer mbb = ref.get(); if (mbb != null) { try { mbb.force(); } catch(Exception ign) {} } } } static class BufferReference<T extends MappedByteBuffer> extends SoftReference<T> { final int idx; public BufferReference(final int idx, final T referent) { super(referent); this.idx = idx; } } // ========= Exceptions ========= /** * Exception throwed when store is in invalid state (closed) */ public static class InvalidStateException extends RuntimeException { private static final long serialVersionUID = 42L; } // ========= END ========= }