View Javadoc

1   /**
2    * JDBM LICENSE v1.00
3    *
4    * Redistribution and use of this software and associated documentation
5    * ("Software"), with or without modification, are permitted provided
6    * that the following conditions are met:
7    *
8    * 1. Redistributions of source code must retain copyright
9    *    statements and notices.  Redistributions must also contain a
10   *    copy of this document.
11   *
12   * 2. Redistributions in binary form must reproduce the
13   *    above copyright notice, this list of conditions and the
14   *    following disclaimer in the documentation and/or other
15   *    materials provided with the distribution.
16   *
17   * 3. The name "JDBM" must not be used to endorse or promote
18   *    products derived from this Software without prior written
19   *    permission of Cees de Groot.  For written permission,
20   *    please contact cg@cdegroot.com.
21   *
22   * 4. Products derived from this Software may not be called "JDBM"
23   *    nor may "JDBM" appear in their names without prior written
24   *    permission of Cees de Groot.
25   *
26   * 5. Due credit should be given to the JDBM Project
27   *    (http://jdbm.sourceforge.net/).
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
30   * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
31   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
32   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
33   * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
34   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
35   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
36   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
37   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
38   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
40   * OF THE POSSIBILITY OF SUCH DAMAGE.
41   *
42   * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
43   * Contributions are Copyright (C) 2000 by their associated contributors.
44   *
45   * $Id: RecordFile.java,v 1.6 2005/06/25 23:12:32 doomdark Exp $
46   */
47  
48  package jdbm.recman;
49  
50  import java.io.*;
51  import java.util.*;
52  
53  /**
54   *  This class represents a random access file as a set of fixed size
55   *  records. Each record has a physical record number, and records are
56   *  cached in order to improve access.
57   *<p>
58   *  The set of dirty records on the in-use list constitutes a transaction.
59   *  Later on, we will send these records to some recovery thingy.
60   */
61  public final class RecordFile {
62      final TransactionManager txnMgr;
63  
64      // Todo: reorganize in hashes and fifos as necessary.
65      // free -> inUse -> dirty -> inTxn -> free
66      // free is a cache, thus a FIFO. The rest are hashes.
67      private final LinkedList free = new LinkedList();
68      private final HashMap inUse = new HashMap();
69      private final HashMap dirty = new HashMap();
70      private final HashMap inTxn = new HashMap();
71  
72      // transactions disabled?
73      private boolean transactionsDisabled = false;
74  
75      /** The length of a single block. */
76      public final static int BLOCK_SIZE = 8192;//4096;
77  
78      /** The extension of a record file */
79      final static String extension = ".db";
80  
81      /** A block of clean data to wipe clean pages. */
82      final static byte[] cleanData = new byte[BLOCK_SIZE];
83  
84      private RandomAccessFile file;
85      private final String fileName;
86  
87      /**
88       *  Creates a new object on the indicated filename. The file is
89       *  opened in read/write mode.
90       *
91       *  @param fileName the name of the file to open or create, without
92       *         an extension.
93       *  @throws IOException whenever the creation of the underlying
94       *          RandomAccessFile throws it.
95       */
96      RecordFile(String fileName) throws IOException {
97          this.fileName = fileName;
98          file = new RandomAccessFile(fileName + extension, "rw");
99          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 }