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    }