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: TransactionManager.java,v 1.7 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 manages the transaction log that belongs to every
055     *  {@link RecordFile}. The transaction log is either clean, or
056     *  in progress. In the latter case, the transaction manager
057     *  takes care of a roll forward.
058     *<p>
059     *  Implementation note: this is a proof-of-concept implementation
060     *  which hasn't been optimized for speed. For instance, all sorts
061     *  of streams are created for every transaction.
062     */
063    // TODO: Handle the case where we are recovering lg9 and lg0, were we
064    // should start with lg9 instead of lg0!
065    
066    public final class TransactionManager {
067        private RecordFile owner;
068    
069        // streams for transaction log.
070        private FileOutputStream fos;
071        private ObjectOutputStream oos;
072    
073        /** 
074         * By default, we keep 10 transactions in the log file before
075         * synchronizing it with the main database file.
076         */
077        static final int DEFAULT_TXNS_IN_LOG = 10;
078    
079        /** 
080         * Maximum number of transactions before the log file is
081         * synchronized with the main database file.
082         */
083        private int _maxTxns = DEFAULT_TXNS_IN_LOG;
084    
085        /**
086         * In-core copy of transactions. We could read everything back from
087         * the log file, but the RecordFile needs to keep the dirty blocks in
088         * core anyway, so we might as well point to them and spare us a lot
089         * of hassle.
090         */
091        private ArrayList[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
092        private int curTxn = -1;
093    
094        /** Extension of a log file. */
095        static final String extension = ".lg";
096    
097        /**
098         *  Instantiates a transaction manager instance. If recovery
099         *  needs to be performed, it is done.
100         *
101         *  @param owner the RecordFile instance that owns this transaction mgr.
102         */
103        TransactionManager(RecordFile owner) throws IOException {
104            this.owner = owner;
105            recover();
106            open();
107        }
108    
109        
110        /**
111         * Synchronize log file data with the main database file.
112         * <p>
113         * After this call, the main database file is guaranteed to be 
114         * consistent and guaranteed to be the only file needed for 
115         * backup purposes.
116         */
117        public void synchronizeLog()
118            throws IOException
119        {
120            synchronizeLogFromMemory();
121        }
122    
123        
124        /**
125         * Set the maximum number of transactions to record in
126         * the log (and keep in memory) before the log is
127         * synchronized with the main database file.
128         * <p>
129         * This method must be called while there are no
130         * pending transactions in the log.
131         */
132        public void setMaximumTransactionsInLog( int maxTxns )
133            throws IOException
134        {
135            if ( maxTxns <= 0 ) {
136                throw new IllegalArgumentException( 
137                    "Argument 'maxTxns' must be greater than 0." );
138            }
139            if ( curTxn != -1 ) {
140                throw new IllegalStateException( 
141                    "Cannot change setting while transactions are pending in the log" );
142            }
143            _maxTxns = maxTxns;
144            txns = new ArrayList[ maxTxns ];
145        }
146    
147        
148        /** Builds logfile name  */
149        private String makeLogName() {
150            return owner.getFileName() + extension;
151        }
152    
153    
154        /** Synchs in-core transactions to data file and opens a fresh log */
155        private void synchronizeLogFromMemory() throws IOException {
156            close();
157    
158            TreeSet blockList = new TreeSet( new BlockIoComparator() );
159    
160            int numBlocks = 0;
161            int writtenBlocks = 0;
162            for (int i = 0; i < _maxTxns; i++) {
163                if (txns[i] == null)
164                    continue;
165                // Add each block to the blockList, replacing the old copy of this
166                // block if necessary, thus avoiding writing the same block twice
167                for (Iterator k = txns[i].iterator(); k.hasNext(); ) {
168                    BlockIo block = (BlockIo)k.next();
169                    if ( blockList.contains( block ) ) {
170                        block.decrementTransactionCount();
171                    }
172                    else {
173                        writtenBlocks++;
174                        boolean result = blockList.add( block );
175                    }
176                    numBlocks++;
177                }
178    
179                txns[i] = null;
180            }
181            // Write the blocks from the blockList to disk
182            synchronizeBlocks(blockList.iterator(), true);
183    
184            owner.sync();
185            open();
186        }
187    
188    
189        /** Opens the log file */
190        private void open() throws IOException {
191            fos = new FileOutputStream(makeLogName());
192            oos = new ObjectOutputStream(fos);
193            oos.writeShort(Magic.LOGFILE_HEADER);
194            oos.flush();
195            curTxn = -1;
196        }
197    
198        /** Startup recovery on all files */
199        private void recover() throws IOException {
200            String logName = makeLogName();
201            File logFile = new File(logName);
202            if (!logFile.exists())
203                return;
204            if (logFile.length() == 0) {
205                logFile.delete();
206                return;
207            }
208    
209            FileInputStream fis = new FileInputStream(logFile);
210            ObjectInputStream ois = new ObjectInputStream(fis);
211    
212            try {
213                if (ois.readShort() != Magic.LOGFILE_HEADER) {
214                    ois.close();
215                    throw new Error("Bad magic on log file");
216                }
217            } catch (IOException e) {
218                // corrupted/empty logfile
219                ois.close();
220                logFile.delete();
221                return;
222            }
223    
224            while (true) {
225                ArrayList blocks = null;
226                try {
227                    blocks = (ArrayList) ois.readObject();
228                } catch (ClassNotFoundException e) {
229                    ois.close();
230                    throw new Error("Unexcepted exception: " + e);
231                } catch (IOException e) {
232                    // corrupted logfile, ignore rest of transactions
233                    break;
234                }
235                synchronizeBlocks(blocks.iterator(), false);
236    
237                // ObjectInputStream must match exactly each
238                // ObjectOutputStream created during writes
239                try {
240                    ois = new ObjectInputStream(fis);
241                } catch (IOException e) {
242                    // corrupted logfile, ignore rest of transactions
243                    break;
244                }
245            }
246            owner.sync();
247            ois.close();
248            logFile.delete();
249        }
250    
251        /** Synchronizes the indicated blocks with the owner. */
252        private void synchronizeBlocks(Iterator blockIterator, boolean fromCore)
253        throws IOException {
254            // write block vector elements to the data file.
255            while ( blockIterator.hasNext() ) {
256                BlockIo cur = (BlockIo)blockIterator.next();
257                owner.synch(cur);
258                if (fromCore) {
259                    cur.decrementTransactionCount();
260                    if (!cur.isInTransaction()) {
261                        owner.releaseFromTransaction(cur, true);
262                    }
263                }
264            }
265        }
266    
267    
268        /** Set clean flag on the blocks. */
269        private void setClean(ArrayList blocks)
270        throws IOException {
271            for (Iterator k = blocks.iterator(); k.hasNext(); ) {
272                BlockIo cur = (BlockIo) k.next();
273                cur.setClean();
274            }
275        }
276    
277        /** Discards the indicated blocks and notify the owner. */
278        private void discardBlocks(ArrayList blocks)
279        throws IOException {
280            for (Iterator k = blocks.iterator(); k.hasNext(); ) {
281                BlockIo cur = (BlockIo) k.next();
282                cur.decrementTransactionCount();
283                if (!cur.isInTransaction()) {
284                    owner.releaseFromTransaction(cur, false);
285                }
286            }
287        }
288    
289        /**
290         *  Starts a transaction. This can block if all slots have been filled
291         *  with full transactions, waiting for the synchronization thread to
292         *  clean out slots.
293         */
294        void start() throws IOException {
295            curTxn++;
296            if (curTxn == _maxTxns) {
297                synchronizeLogFromMemory();
298                curTxn = 0;
299            }
300            txns[curTxn] = new ArrayList();
301        }
302    
303        /**
304         *  Indicates the block is part of the transaction.
305         */
306        void add(BlockIo block) throws IOException {
307            block.incrementTransactionCount();
308            txns[curTxn].add(block);
309        }
310    
311        /**
312         *  Commits the transaction to the log file.
313         */
314        void commit() throws IOException {
315            oos.writeObject(txns[curTxn]);
316            sync();
317    
318            // set clean flag to indicate blocks have been written to log
319            setClean(txns[curTxn]);
320    
321            // reset ObjectOutputStream in order to store
322            // newer states of BlockIo
323            oos = new ObjectOutputStream(fos);
324            oos.reset();
325        }
326    
327        /** Flushes and syncs */
328        private void sync() throws IOException {
329            oos.flush();
330            fos.flush();
331            fos.getFD().sync();
332        }
333    
334        /**
335         *  Shutdowns the transaction manager. Resynchronizes outstanding
336         *  logs.
337         */
338        void shutdown() throws IOException {
339            synchronizeLogFromMemory();
340            close();
341        }
342    
343        /**
344         *  Closes open files.
345         */
346        private void close() throws IOException {
347            sync();
348            oos.close();
349            fos.close();
350            oos = null;
351            fos = null;
352        }
353    
354        /**
355         * Force closing the file without synchronizing pending transaction data.
356         * Used for testing purposes only.
357         */
358        void forceClose() throws IOException {
359            oos.close();
360            fos.close();
361            oos = null;
362            fos = null;
363        }
364    
365        /**
366         * Use the disk-based transaction log to synchronize the data file.
367         * Outstanding memory logs are discarded because they are believed
368         * to be inconsistent.
369         */
370        void synchronizeLogFromDisk() throws IOException {
371            close();
372    
373            for ( int i=0; i < _maxTxns; i++ ) {
374                if (txns[i] == null)
375                    continue;
376                discardBlocks(txns[i]);
377                txns[i] = null;
378            }
379    
380            recover();
381            open();
382        }
383    
384    
385        /** INNER CLASS.
386         *  Comparator class for use by the tree set used to store the blocks
387         *  to write for this transaction.  The BlockIo objects are ordered by
388         *  their blockIds.
389         */
390        public static class BlockIoComparator
391            implements Comparator
392        {
393    
394            public int compare( Object o1, Object o2 ) {
395                BlockIo block1 = (BlockIo)o1;
396                BlockIo block2 = (BlockIo)o2;
397                int result = 0;
398                if ( block1.getBlockId() == block2.getBlockId() ) {
399                    result = 0;
400                }
401                else if ( block1.getBlockId() < block2.getBlockId() ) {
402                    result = -1;
403                }
404                else {
405                    result = 1;
406                }
407                return result;
408            }
409    
410            public boolean equals(Object obj) {
411                return super.equals(obj);
412            }
413        } // class BlockIOComparator
414    
415    }