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    package jdbm.recman;
048    
049    
050    import java.io.*;
051    import java.util.*;
052    
053    import org.apache.directory.server.i18n.I18n;
054    
055    
056    /**
057     *  This class represents a random access file as a set of fixed size
058     *  records. Each record has a physical record number, and records are
059     *  cached in order to improve access.
060     *<p>
061     *  The set of dirty records on the in-use list constitutes a transaction.
062     *  Later on, we will send these records to some recovery thingy.
063     */
064    public final class RecordFile 
065    {
066        final TransactionManager txnMgr;
067    
068        // state transitions: free -> inUse -> dirty -> inTxn -> free
069        // free is a cache, thus a FIFO. The rest are hashes.
070        private final LinkedList<BlockIo> free = new LinkedList<BlockIo>();
071        private final HashMap<Long,BlockIo> inUse = new HashMap<Long,BlockIo>();
072        private final HashMap<Long,BlockIo> dirty = new HashMap<Long,BlockIo>();
073        private final HashMap<Long,BlockIo> inTxn = new HashMap<Long,BlockIo>();
074    
075        // transactions disabled?
076        private boolean transactionsDisabled = false;
077    
078        /** The length of a single block. */
079        public final static int BLOCK_SIZE = 8192;//4096;
080    
081        /** The extension of a record file */
082        final static String extension = ".db";
083    
084        /** A block of clean data to wipe clean pages. */
085        final static byte[] cleanData = new byte[BLOCK_SIZE];
086    
087        private RandomAccessFile file;
088        private final String fileName;
089    
090        
091        /**
092         * Creates a new object on the indicated filename. The file is
093         * opened in read/write mode.
094         *
095         * @param fileName the name of the file to open or create, without
096         *        an extension.
097         * @throws IOException whenever the creation of the underlying
098         *         RandomAccessFile throws it.
099         */
100        RecordFile ( String fileName ) throws IOException 
101        {
102            this.fileName = fileName;
103            file = new RandomAccessFile(fileName + extension, "rw");
104            txnMgr = new TransactionManager( this );
105        }
106    
107        
108        /**
109         * Returns the file name.
110         */
111        String getFileName() 
112        {
113            return fileName;
114        }
115    
116        
117        /**
118         * Disables transactions: doesn't sync and doesn't use the
119         * transaction manager.
120         */
121        void disableTransactions() 
122        {
123            transactionsDisabled = true;
124        }
125    
126        
127        /**
128         * Gets a block from the file. The returned byte array is the in-memory 
129         * copy of the record, and thus can be written (and subsequently released 
130         * with a dirty flag in order to write the block back).
131         *
132         * @param blockid The record number to retrieve.
133         */
134         BlockIo get( long blockid ) throws IOException 
135         {
136             // try in transaction list, dirty list, free list
137             
138             BlockIo node = inTxn.get( blockid );
139             if ( node != null ) 
140             {
141                 inTxn.remove( blockid );
142                 inUse.put( blockid, node );
143                 return node;
144             }
145             
146             node = dirty.get( blockid );
147             if ( node != null ) 
148             {
149                 dirty.remove( blockid );
150                 inUse.put( blockid, node );
151                 return node;
152             }
153             
154             for ( Iterator<BlockIo> i = free.iterator(); i.hasNext(); ) 
155             {
156                 BlockIo cur = i.next();
157                 if ( cur.getBlockId() == blockid ) 
158                 {
159                     node = cur;
160                     i.remove();
161                     inUse.put( blockid, node );
162                     return node;
163                 }
164             }
165    
166             // sanity check: can't be on in use list
167             if ( inUse.get( blockid ) != null ) 
168             {
169                 throw new Error( I18n.err( I18n.ERR_554, blockid ) );
170             }
171    
172             // get a new node and read it from the file
173             node = getNewNode( blockid );
174             long offset = blockid * BLOCK_SIZE;
175             if ( file.length() > 0 && offset <= file.length() ) 
176             {
177                 read( file, offset, node.getData(), BLOCK_SIZE );
178             } 
179             else 
180             {
181                 System.arraycopy( cleanData, 0, node.getData(), 0, BLOCK_SIZE );
182             }
183             
184             inUse.put( blockid, node );
185             node.setClean();
186             return node;
187         }
188    
189    
190        /**
191         * Releases a block.
192         *
193         * @param blockid The record number to release.
194         * @param isDirty If true, the block was modified since the get().
195         */
196        void release( long blockid, boolean isDirty ) throws IOException 
197        {
198            BlockIo node = inUse.get( blockid );
199            
200            if ( node == null )
201            {
202                throw new IOException( I18n.err( I18n.ERR_555, blockid ) );
203            }
204            
205            if ( ! node.isDirty() && isDirty )
206            {
207                node.setDirty();
208            }
209                
210            release( node );
211        }
212    
213        
214        /**
215         * Releases a block.
216         *
217         * @param block The block to release.
218         */
219        void release( BlockIo block ) 
220        {
221            inUse.remove( block.getBlockId() );
222            
223            if ( block.isDirty() ) 
224            {
225                // System.out.println( "Dirty: " + key + block );
226                dirty.put( block.getBlockId(), block );
227            } 
228            else 
229            {
230                if ( ! transactionsDisabled && block.isInTransaction() ) 
231                {
232                    inTxn.put( block.getBlockId(), block );
233                } 
234                else 
235                {
236                    free.add( block );
237                }
238            }
239        }
240        
241    
242        /**
243         * Discards a block (will not write the block even if it's dirty)
244         *
245         * @param block The block to discard.
246         */
247        void discard( BlockIo block ) 
248        {
249            inUse.remove( block.getBlockId() );
250    
251            // note: block not added to free list on purpose, because
252            //       it's considered invalid
253        }
254    
255        
256        /**
257         * Commits the current transaction by flushing all dirty buffers to disk.
258         */
259        void commit() throws IOException 
260        {
261            // debugging...
262            if ( ! inUse.isEmpty() && inUse.size() > 1 ) 
263            {
264                showList( inUse.values().iterator() );
265                throw new Error( I18n.err( I18n.ERR_556, inUse.size() ) );
266            }
267    
268            //  System.out.println("committing...");
269    
270            if ( dirty.size() == 0 ) 
271            {
272                // if no dirty blocks, skip commit process
273                return;
274            }
275    
276            
277            if ( ! transactionsDisabled ) 
278            {
279                txnMgr.start();
280            }
281    
282            
283            for ( Iterator<BlockIo> i = dirty.values().iterator(); i.hasNext(); ) 
284            {
285                BlockIo node = ( BlockIo ) i.next();
286                i.remove();
287                
288                // System.out.println("node " + node + " map size now " + dirty.size());
289                if ( transactionsDisabled ) 
290                {
291                    long offset = node.getBlockId() * BLOCK_SIZE;
292                    file.seek( offset );
293                    file.write( node.getData() );
294                    node.setClean();
295                    free.add( node );
296                }
297                else 
298                {
299                    txnMgr.add( node );
300                    inTxn.put( node.getBlockId(), node );
301                }
302            }
303    
304            if ( ! transactionsDisabled ) 
305            {
306                txnMgr.commit();
307            }
308        }
309    
310        
311        /**
312         * Rollback the current transaction by discarding all dirty buffers
313         */
314        void rollback() throws IOException 
315        {
316            // debugging...
317            if ( ! inUse.isEmpty() ) 
318            {
319                showList( inUse.values().iterator() );
320                throw new Error( I18n.err( I18n.ERR_557, inUse.size() ) );
321            }
322        
323            //  System.out.println("rollback...");
324            dirty.clear();
325    
326            txnMgr.synchronizeLogFromDisk();
327    
328            if ( ! inTxn.isEmpty() ) 
329            {
330                showList( inTxn.values().iterator() );
331                throw new Error( I18n.err( I18n.ERR_558, inTxn.size() ) );
332            };
333        }
334    
335        
336        /**
337         * Commits and closes file.
338         */
339        void close() throws IOException 
340        {
341            if ( ! dirty.isEmpty() ) 
342            {
343                commit();
344            }
345            
346            txnMgr.shutdown();
347    
348            if ( ! inTxn.isEmpty() ) 
349            {
350                showList( inTxn.values().iterator() );
351                throw new Error( I18n.err( I18n.ERR_559 ) );
352            }
353    
354            // these actually ain't that bad in a production release
355            if ( ! dirty.isEmpty() ) 
356            {
357                System.out.println( "ERROR: dirty blocks at close time" );
358                showList( dirty.values().iterator() );
359                throw new Error( I18n.err( I18n.ERR_560 ) );
360            }
361            
362            if ( ! inUse.isEmpty() ) 
363            {
364                System.out.println( "ERROR: inUse blocks at close time" );
365                showList( inUse.values().iterator() );
366                throw new Error( I18n.err( I18n.ERR_561 ) );
367            }
368    
369            // debugging stuff to keep an eye on the free list
370            // System.out.println("Free list size:" + free.size());
371            file.close();
372            file = null;
373        }
374    
375    
376        /**
377         * Force closing the file and underlying transaction manager.
378         * Used for testing purposed only.
379         */
380        void forceClose() throws IOException 
381        {
382          txnMgr.forceClose();
383          file.close();
384        }
385    
386        
387        /**
388         * Prints contents of a list
389         */
390        private void showList( Iterator<BlockIo> i ) 
391        {
392            int cnt = 0;
393            while ( i.hasNext() ) 
394            {
395                System.out.println( "elem " + cnt + ": " + i.next() );
396                cnt++;
397            }
398        }
399    
400    
401        /**
402         * Returns a new node. The node is retrieved (and removed) from the 
403         * released list or created new.
404         */
405        private BlockIo getNewNode( long blockid ) throws IOException 
406        {
407            BlockIo retval = null;
408    
409            if ( ! free.isEmpty() ) 
410            {
411                retval = ( BlockIo ) free.removeFirst();
412            }
413            
414            if ( retval == null )
415            {
416                retval = new BlockIo( 0, new byte[BLOCK_SIZE] );
417            }
418            
419            retval.setBlockId(blockid);
420            retval.setView( null );
421            return retval;
422        }
423        
424    
425        /**
426         * Synchronizes a node to disk. This is called by the transaction manager's
427         * synchronization code.
428         */
429        void synch( BlockIo node ) throws IOException 
430        {
431            byte[] data = node.getData();
432            if ( data != null ) 
433            {
434                long offset = node.getBlockId() * BLOCK_SIZE;
435                file.seek( offset );
436                file.write( data );
437            }
438        }
439    
440        
441        /**
442         * Releases a node from the transaction list, if it was sitting there.
443         *
444         * @param recycle true if block data can be reused
445         */
446        void releaseFromTransaction( BlockIo node, boolean recycle ) throws IOException 
447        {
448            if ( ( inTxn.remove( node.getBlockId() ) != null ) && recycle ) 
449            {
450                free.add( node );
451            }
452        }
453        
454    
455        /**
456         * Synchronizes the file.
457         */
458        void sync() throws IOException 
459        {
460            file.getFD().sync();
461        }
462    
463    
464        /**
465         * Utility method: Read a block from a RandomAccessFile
466         */
467        private static void read( RandomAccessFile file, long offset, byte[] buffer, int nBytes ) throws IOException 
468        {
469            file.seek( offset );
470            int remaining = nBytes;
471            int pos = 0;
472            while ( remaining > 0 ) 
473            {
474                int read = file.read( buffer, pos, remaining );
475                if ( read == -1 ) 
476                {
477                    System.arraycopy( cleanData, 0, buffer, pos, remaining );
478                    break;
479                }
480                remaining -= read;
481                pos += read;
482            }
483        }
484    }