/******************************************************************************* * Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek * * 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 de.mxro.thrd.jdbm2V22.recman; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.OverlappingFileLockException; import java.util.ArrayList; import java.util.Iterator; import de.mxro.thrd.jdbm2V22.helper.LongHashMap; /** * This class represents a random access file as a set of fixed size * records. Each record has a physical record number, and records are * cached in order to improve access. *<p> * The set of dirty records on the in-use list constitutes a transaction. * Later on, we will send these records to some recovery thingy. *<p> * RecordFile is splited between more files, each with max size 1GB. */ final class RecordFile { final TransactionManager txnMgr; // Todo: reorganize in hashes and fifos as necessary. // free -> inUse -> dirty -> inTxn -> free // free is a cache, thus a FIFO. The rest are hashes. private final LongHashMap<BlockIo> free = new LongHashMap<BlockIo>(); /** * Blocks currently locked for read/update ops. When released the block goes * to the dirty or clean list, depending on a flag. The file header block is * normally locked plus the block that is currently being read or modified. * * @see BlockIo#isDirty() */ private final LongHashMap<BlockIo> inUse = new LongHashMap<BlockIo>(); /** * Blocks whose state is dirty. */ private final LongHashMap<BlockIo> dirty = new LongHashMap<BlockIo>(); /** * Blocks in a <em>historical</em> transaction(s) that have been written * onto the log but which have not yet been committed to the database. */ private final LongHashMap<BlockIo> inTxn = new LongHashMap<BlockIo>(); // transactions disabled? private boolean transactionsDisabled = false; static final int DEFAULT_BLOCK_SIZE = 4096; // /** The length of a single block. */ final int BLOCK_SIZE ;//= 8192;//4096; // /** maximal file size not rounded to block size */ private final static long _FILESIZE = 1000000000l; private final long MAX_FILE_SIZE;// = _FILESIZE - _FILESIZE%BLOCK_SIZE; /** A block of clean data to wipe clean pages. */ final byte[] cleanData; private ArrayList<RandomAccessFile> rafs = new ArrayList<RandomAccessFile>(); private final String fileName; RecordFile(String fileName) throws IOException { this(fileName,DEFAULT_BLOCK_SIZE); } /** * Creates a new object on the indicated filename. The file is * opened in read/write mode. * * @param fileName the name of the file to open or create, without * an extension. * @throws IOException whenever the creation of the underlying * RandomAccessFile throws it. */ RecordFile(String fileName, int blockSize) throws IOException { this.BLOCK_SIZE = blockSize; MAX_FILE_SIZE = _FILESIZE - _FILESIZE%BLOCK_SIZE; cleanData = new byte[BLOCK_SIZE]; this.fileName = fileName; //make sure first file can be opened //lock it try{ getRaf(0).getChannel().tryLock(); }catch(IOException e){ throw new IOException("Could not lock DB file: "+fileName,e); }catch(OverlappingFileLockException e){ throw new IOException("Could not lock DB file: "+fileName,e); } txnMgr = new TransactionManager(this); } RandomAccessFile getRaf(long offset) throws IOException { int fileNumber = (int) (offset/MAX_FILE_SIZE); //increase capacity of array lists if needed for(int i = rafs.size();i<=fileNumber;i++){ rafs.add(null); } RandomAccessFile ret = rafs.get(fileNumber); if(ret == null){ String name = fileName+"."+fileNumber; ret = new RandomAccessFile(name, "rw"); rafs.set(fileNumber, ret); } return ret; } /** * Returns the file name. */ String getFileName() { return fileName; } /** * Disables transactions: doesn't sync and doesn't use the * transaction manager. */ void disableTransactions() { transactionsDisabled = true; } /** * Gets a block from the file. The returned byte array is * the in-memory copy of the record, and thus can be written * (and subsequently released with a dirty flag in order to * write the block back). * * @param blockid The record number to retrieve. */ BlockIo get(long blockid) throws IOException { // try in transaction list, dirty list, free list BlockIo node = inTxn.get(blockid); if (node != null) { inTxn.remove(blockid); inUse.put(blockid, node); return node; } node = dirty.get(blockid); if (node != null) { dirty.remove(blockid); inUse.put(blockid, node); return node; } BlockIo cur = free.get(blockid); if(cur!=null){ node = cur; free.remove(blockid); inUse.put(blockid, node); return node; } // sanity check: can't be on in use list if (inUse.get(blockid) != null) { throw new Error("double get for block " + blockid ); } // get a new node and read it from the file node = getNewNode(blockid); long offset = blockid * BLOCK_SIZE; RandomAccessFile file = getRaf(offset); read(file, offset%MAX_FILE_SIZE, node.getData(), BLOCK_SIZE); inUse.put(blockid, node); node.setClean(); return node; } /** * Releases a block. * * @param blockid The record number to release. * @param isDirty If true, the block was modified since the get(). */ void release(long blockid, boolean isDirty) throws IOException { BlockIo node = inUse.get(blockid); if (node == null) throw new IOException("bad blockid " + blockid + " on release"); if (!node.isDirty() && isDirty) node.setDirty(); release(node); } /** * Releases a block. * * @param block The block to release. */ void release(BlockIo block) { final long key =block.getBlockId(); inUse.remove(key); if (block.isDirty()) { // System.out.println( "Dirty: " + key + block ); dirty.put(key, block); } else { if (!transactionsDisabled && block.isInTransaction()) { inTxn.put(key, block); } else { free.put(key,block); } } } /** * Discards a block (will not write the block even if it's dirty) * * @param block The block to discard. */ void discard(BlockIo block) { long key = block.getBlockId(); inUse.remove(key); // note: block not added to free list on purpose, because // it's considered invalid } /** * Commits the current transaction by flushing all dirty buffers * to disk. */ void commit() throws IOException { // debugging... if (!inUse.isEmpty() && inUse.size() > 1) { showList(inUse.valuesIterator()); throw new Error("in use list not empty at commit time (" + inUse.size() + ")"); } // System.out.println("committing..."); if ( dirty.size() == 0 ) { // if no dirty blocks, skip commit process return; } if (!transactionsDisabled) { txnMgr.start(); } for (Iterator<BlockIo> i = dirty.valuesIterator(); i.hasNext(); ) { BlockIo node = i.next(); i.remove(); // System.out.println("node " + node + " map size now " + dirty.size()); if (transactionsDisabled) { long offset = node.getBlockId() * BLOCK_SIZE; RandomAccessFile file = getRaf(offset); file.seek(offset % MAX_FILE_SIZE); file.write(node.getData()); node.setClean(); free.put(node.getBlockId(),node); } else { txnMgr.add(node); inTxn.put(node.getBlockId(), node); } } if (!transactionsDisabled) { txnMgr.commit(); } } /** * Rollback the current transaction by discarding all dirty buffers */ void rollback() throws IOException { // debugging... if (!inUse.isEmpty()) { showList(inUse.valuesIterator()); throw new Error("in use list not empty at rollback time (" + inUse.size() + ")"); } // System.out.println("rollback..."); dirty.clear(); txnMgr.synchronizeLogFromDisk(); if (!inTxn.isEmpty()) { showList(inTxn.valuesIterator()); throw new Error("in txn list not empty at rollback time (" + inTxn.size() + ")"); }; } /** * Commits and closes file. */ void close() throws IOException { if (!dirty.isEmpty()) { commit(); } txnMgr.shutdown(); if (!inTxn.isEmpty()) { showList(inTxn.valuesIterator()); throw new Error("In transaction not empty"); } // these actually ain't that bad in a production release if (!dirty.isEmpty()) { System.out.println("ERROR: dirty blocks at close time"); showList(dirty.valuesIterator()); throw new Error("Dirty blocks at close time"); } if (!inUse.isEmpty()) { System.out.println("ERROR: inUse blocks at close time"); showList(inUse.valuesIterator()); throw new Error("inUse blocks at close time"); } for(RandomAccessFile f :rafs){ if(f!=null) f.close(); } rafs = null; } /** * Force closing the file and underlying transaction manager. * Used for testing purposed only. */ void forceClose() throws IOException { txnMgr.forceClose(); for(RandomAccessFile f :rafs){ if(f!=null) f.close(); } rafs = null; } /** * Prints contents of a list */ protected void showList(Iterator<BlockIo> i) { int cnt = 0; while (i.hasNext()) { System.out.println("elem " + cnt + ": " + i.next()); cnt++; } } /** * Returns a new node. The node is retrieved (and removed) * from the released list or created new. */ private BlockIo getNewNode(long blockid) throws IOException { BlockIo retval = null; if (!free.isEmpty()) { Iterator<BlockIo> it = free.valuesIterator(); retval = it.next(); it.remove(); } if (retval == null) retval = new BlockIo(0, new byte[BLOCK_SIZE]); retval.setBlockId(blockid); retval.setView(null); return retval; } /** * Synchs a node to disk. This is called by the transaction manager's * synchronization code. */ void synch(BlockIo node) throws IOException { byte[] data = node.getData(); if (data != null) { long offset = node.getBlockId() * BLOCK_SIZE; RandomAccessFile file = getRaf(offset); file.seek(offset % MAX_FILE_SIZE); file.write(data); } } /** * Releases a node from the transaction list, if it was sitting * there. * * @param recycle true if block data can be reused */ void releaseFromTransaction(BlockIo node, boolean recycle) throws IOException { long key = node.getBlockId(); if ((inTxn.remove(key) != null) && recycle) { free.put(key,node); } } /** * Synchronizes the file. */ void sync() throws IOException { for(RandomAccessFile file:rafs) if(file!=null) file.getFD().sync(); } /** * Utility method: Read a block from a RandomAccessFile */ protected void read(RandomAccessFile file, long offset, byte[] buffer, int nBytes) throws IOException { file.seek(offset); int remaining = nBytes; int pos = 0; while (remaining > 0) { int read = file.read(buffer, pos, remaining); if (read == -1) { System.arraycopy(cleanData, 0, buffer, pos, remaining); break; } remaining -= read; pos += read; } } }