001 /**
002 * JDBM LICENSE v1.00
003 *
004 * Redistribution and use of this software and associated documentation
005 * ("Software"), with or without modification, are permitted provided
006 * that the following conditions are met:
007 *
008 * 1. Redistributions of source code must retain copyright
009 * statements and notices. Redistributions must also contain a
010 * copy of this document.
011 *
012 * 2. Redistributions in binary form must reproduce the
013 * above copyright notice, this list of conditions and the
014 * following disclaimer in the documentation and/or other
015 * materials provided with the distribution.
016 *
017 * 3. The name "JDBM" must not be used to endorse or promote
018 * products derived from this Software without prior written
019 * permission of Cees de Groot. For written permission,
020 * please contact cg@cdegroot.com.
021 *
022 * 4. Products derived from this Software may not be called "JDBM"
023 * nor may "JDBM" appear in their names without prior written
024 * permission of Cees de Groot.
025 *
026 * 5. Due credit should be given to the JDBM Project
027 * (http://jdbm.sourceforge.net/).
028 *
029 * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
030 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
031 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
032 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
033 * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
034 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
035 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
036 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
037 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
038 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
039 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
040 * OF THE POSSIBILITY OF SUCH DAMAGE.
041 *
042 * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
043 * Contributions are Copyright (C) 2000 by their associated contributors.
044 *
045 * $Id: RecordFile.java,v 1.6 2005/06/25 23:12:32 doomdark Exp $
046 */
047
048 package jdbm.recman;
049
050 import java.io.*;
051 import java.util.*;
052
053 /**
054 * This class represents a random access file as a set of fixed size
055 * records. Each record has a physical record number, and records are
056 * cached in order to improve access.
057 *<p>
058 * The set of dirty records on the in-use list constitutes a transaction.
059 * Later on, we will send these records to some recovery thingy.
060 */
061 public final class RecordFile {
062 final TransactionManager txnMgr;
063
064 // Todo: reorganize in hashes and fifos as necessary.
065 // free -> inUse -> dirty -> inTxn -> free
066 // free is a cache, thus a FIFO. The rest are hashes.
067 private final LinkedList free = new LinkedList();
068 private final HashMap inUse = new HashMap();
069 private final HashMap dirty = new HashMap();
070 private final HashMap inTxn = new HashMap();
071
072 // transactions disabled?
073 private boolean transactionsDisabled = false;
074
075 /** The length of a single block. */
076 public final static int BLOCK_SIZE = 8192;//4096;
077
078 /** The extension of a record file */
079 final static String extension = ".db";
080
081 /** A block of clean data to wipe clean pages. */
082 final static byte[] cleanData = new byte[BLOCK_SIZE];
083
084 private RandomAccessFile file;
085 private final String fileName;
086
087 /**
088 * Creates a new object on the indicated filename. The file is
089 * opened in read/write mode.
090 *
091 * @param fileName the name of the file to open or create, without
092 * an extension.
093 * @throws IOException whenever the creation of the underlying
094 * RandomAccessFile throws it.
095 */
096 RecordFile(String fileName) throws IOException {
097 this.fileName = fileName;
098 file = new RandomAccessFile(fileName + extension, "rw");
099 txnMgr = new TransactionManager(this);
100 }
101
102 /**
103 * Returns the file name.
104 */
105 String getFileName() {
106 return fileName;
107 }
108
109 /**
110 * Disables transactions: doesn't sync and doesn't use the
111 * transaction manager.
112 */
113 void disableTransactions() {
114 transactionsDisabled = true;
115 }
116
117 /**
118 * Gets a block from the file. The returned byte array is
119 * the in-memory copy of the record, and thus can be written
120 * (and subsequently released with a dirty flag in order to
121 * write the block back).
122 *
123 * @param blockid The record number to retrieve.
124 */
125 BlockIo get(long blockid) throws IOException {
126 Long key = new Long(blockid);
127
128 // try in transaction list, dirty list, free list
129 BlockIo node = (BlockIo) inTxn.get(key);
130 if (node != null) {
131 inTxn.remove(key);
132 inUse.put(key, node);
133 return node;
134 }
135 node = (BlockIo) dirty.get(key);
136 if (node != null) {
137 dirty.remove(key);
138 inUse.put(key, node);
139 return node;
140 }
141 for (Iterator i = free.iterator(); i.hasNext(); ) {
142 BlockIo cur = (BlockIo) i.next();
143 if (cur.getBlockId() == blockid) {
144 node = cur;
145 i.remove();
146 inUse.put(key, node);
147 return node;
148 }
149 }
150
151 // sanity check: can't be on in use list
152 if (inUse.get(key) != null) {
153 throw new Error("double get for block " + blockid);
154 }
155
156 // get a new node and read it from the file
157 node = getNewNode(blockid);
158 long offset = blockid * BLOCK_SIZE;
159 if (file.length() > 0 && offset <= file.length()) {
160 read(file, offset, node.getData(), BLOCK_SIZE);
161 } else {
162 System.arraycopy(cleanData, 0, node.getData(), 0, BLOCK_SIZE);
163 }
164 inUse.put(key, node);
165 node.setClean();
166 return node;
167 }
168
169
170 /**
171 * Releases a block.
172 *
173 * @param blockid The record number to release.
174 * @param isDirty If true, the block was modified since the get().
175 */
176 void release(long blockid, boolean isDirty)
177 throws IOException {
178 BlockIo node = (BlockIo) inUse.get(new Long(blockid));
179 if (node == null)
180 throw new IOException("bad blockid " + blockid + " on release");
181 if (!node.isDirty() && isDirty)
182 node.setDirty();
183 release(node);
184 }
185
186 /**
187 * Releases a block.
188 *
189 * @param block The block to release.
190 */
191 void release(BlockIo block) {
192 Long key = new Long(block.getBlockId());
193 inUse.remove(key);
194 if (block.isDirty()) {
195 // System.out.println( "Dirty: " + key + block );
196 dirty.put(key, block);
197 } else {
198 if (!transactionsDisabled && block.isInTransaction()) {
199 inTxn.put(key, block);
200 } else {
201 free.add(block);
202 }
203 }
204 }
205
206 /**
207 * Discards a block (will not write the block even if it's dirty)
208 *
209 * @param block The block to discard.
210 */
211 void discard(BlockIo block) {
212 Long key = new Long(block.getBlockId());
213 inUse.remove(key);
214
215 // note: block not added to free list on purpose, because
216 // it's considered invalid
217 }
218
219 /**
220 * Commits the current transaction by flushing all dirty buffers
221 * to disk.
222 */
223 void commit() throws IOException {
224 // debugging...
225 if (!inUse.isEmpty() && inUse.size() > 1) {
226 showList(inUse.values().iterator());
227 throw new Error("in use list not empty at commit time ("
228 + inUse.size() + ")");
229 }
230
231 // System.out.println("committing...");
232
233 if ( dirty.size() == 0 ) {
234 // if no dirty blocks, skip commit process
235 return;
236 }
237
238 if (!transactionsDisabled) {
239 txnMgr.start();
240 }
241
242 for (Iterator i = dirty.values().iterator(); i.hasNext(); ) {
243 BlockIo node = (BlockIo) i.next();
244 i.remove();
245 // System.out.println("node " + node + " map size now " + dirty.size());
246 if (transactionsDisabled) {
247 long offset = node.getBlockId() * BLOCK_SIZE;
248 file.seek(offset);
249 file.write(node.getData());
250 node.setClean();
251 free.add(node);
252 }
253 else {
254 txnMgr.add(node);
255 inTxn.put(new Long(node.getBlockId()), node);
256 }
257 }
258 if (!transactionsDisabled) {
259 txnMgr.commit();
260 }
261 }
262
263 /**
264 * Rollback the current transaction by discarding all dirty buffers
265 */
266 void rollback() throws IOException {
267 // debugging...
268 if (!inUse.isEmpty()) {
269 showList(inUse.values().iterator());
270 throw new Error("in use list not empty at rollback time ("
271 + inUse.size() + ")");
272 }
273 // System.out.println("rollback...");
274 dirty.clear();
275
276 txnMgr.synchronizeLogFromDisk();
277
278 if (!inTxn.isEmpty()) {
279 showList(inTxn.values().iterator());
280 throw new Error("in txn list not empty at rollback time ("
281 + inTxn.size() + ")");
282 };
283 }
284
285 /**
286 * Commits and closes file.
287 */
288 void close() throws IOException {
289 if (!dirty.isEmpty()) {
290 commit();
291 }
292 txnMgr.shutdown();
293
294 if (!inTxn.isEmpty()) {
295 showList(inTxn.values().iterator());
296 throw new Error("In transaction not empty");
297 }
298
299 // these actually ain't that bad in a production release
300 if (!dirty.isEmpty()) {
301 System.out.println("ERROR: dirty blocks at close time");
302 showList(dirty.values().iterator());
303 throw new Error("Dirty blocks at close time");
304 }
305 if (!inUse.isEmpty()) {
306 System.out.println("ERROR: inUse blocks at close time");
307 showList(inUse.values().iterator());
308 throw new Error("inUse blocks at close time");
309 }
310
311 // debugging stuff to keep an eye on the free list
312 // System.out.println("Free list size:" + free.size());
313 file.close();
314 file = null;
315 }
316
317
318 /**
319 * Force closing the file and underlying transaction manager.
320 * Used for testing purposed only.
321 */
322 void forceClose() throws IOException {
323 txnMgr.forceClose();
324 file.close();
325 }
326
327 /**
328 * Prints contents of a list
329 */
330 private void showList(Iterator i) {
331 int cnt = 0;
332 while (i.hasNext()) {
333 System.out.println("elem " + cnt + ": " + i.next());
334 cnt++;
335 }
336 }
337
338
339 /**
340 * Returns a new node. The node is retrieved (and removed)
341 * from the released list or created new.
342 */
343 private BlockIo getNewNode(long blockid)
344 throws IOException {
345
346 BlockIo retval = null;
347 if (!free.isEmpty()) {
348 retval = (BlockIo) free.removeFirst();
349 }
350 if (retval == null)
351 retval = new BlockIo(0, new byte[BLOCK_SIZE]);
352
353 retval.setBlockId(blockid);
354 retval.setView(null);
355 return retval;
356 }
357
358 /**
359 * Synchs a node to disk. This is called by the transaction manager's
360 * synchronization code.
361 */
362 void synch(BlockIo node) throws IOException {
363 byte[] data = node.getData();
364 if (data != null) {
365 long offset = node.getBlockId() * BLOCK_SIZE;
366 file.seek(offset);
367 file.write(data);
368 }
369 }
370
371 /**
372 * Releases a node from the transaction list, if it was sitting
373 * there.
374 *
375 * @param recycle true if block data can be reused
376 */
377 void releaseFromTransaction(BlockIo node, boolean recycle)
378 throws IOException {
379 Long key = new Long(node.getBlockId());
380 if ((inTxn.remove(key) != null) && recycle) {
381 free.add(node);
382 }
383 }
384
385 /**
386 * Synchronizes the file.
387 */
388 void sync() throws IOException {
389 file.getFD().sync();
390 }
391
392
393 /**
394 * Utility method: Read a block from a RandomAccessFile
395 */
396 private static void read(RandomAccessFile file, long offset,
397 byte[] buffer, int nBytes) throws IOException {
398 file.seek(offset);
399 int remaining = nBytes;
400 int pos = 0;
401 while (remaining > 0) {
402 int read = file.read(buffer, pos, remaining);
403 if (read == -1) {
404 System.arraycopy(cleanData, 0, buffer, pos, remaining);
405 break;
406 }
407 remaining -= read;
408 pos += read;
409 }
410 }
411
412 }