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    import org.apache.directory.server.i18n.I18n;
054    
055    /**
056     *  This class manages the transaction log that belongs to every
057     *  {@link RecordFile}. The transaction log is either clean, or
058     *  in progress. In the latter case, the transaction manager
059     *  takes care of a roll forward.
060     *<p>
061     *  Implementation note: this is a proof-of-concept implementation
062     *  which hasn't been optimized for speed. For instance, all sorts
063     *  of streams are created for every transaction.
064     */
065    // TODO: Handle the case where we are recovering lg9 and lg0, were we
066    // should start with lg9 instead of lg0!
067    
068    public final class TransactionManager {
069        private RecordFile owner;
070    
071        // streams for transaction log.
072        private FileOutputStream fos;
073        private ObjectOutputStream oos;
074    
075        /** 
076         * By default, we keep 10 transactions in the log file before
077         * synchronizing it with the main database file.
078         */
079        static final int DEFAULT_TXNS_IN_LOG = 10;
080    
081        /** 
082         * Maximum number of transactions before the log file is
083         * synchronized with the main database file.
084         */
085        private int _maxTxns = DEFAULT_TXNS_IN_LOG;
086    
087        /**
088         * In-core copy of transactions. We could read everything back from
089         * the log file, but the RecordFile needs to keep the dirty blocks in
090         * core anyway, so we might as well point to them and spare us a lot
091         * of hassle.
092         */
093        private ArrayList[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
094        private int curTxn = -1;
095    
096        /** Extension of a log file. */
097        static final String extension = ".lg";
098    
099        /**
100         *  Instantiates a transaction manager instance. If recovery
101         *  needs to be performed, it is done.
102         *
103         *  @param owner the RecordFile instance that owns this transaction mgr.
104         */
105        TransactionManager(RecordFile owner) throws IOException {
106            this.owner = owner;
107            recover();
108            open();
109        }
110    
111        
112        /**
113         * Synchronize log file data with the main database file.
114         * <p>
115         * After this call, the main database file is guaranteed to be 
116         * consistent and guaranteed to be the only file needed for 
117         * backup purposes.
118         */
119        public void synchronizeLog()
120            throws IOException
121        {
122            synchronizeLogFromMemory();
123        }
124    
125        
126        /**
127         * Set the maximum number of transactions to record in
128         * the log (and keep in memory) before the log is
129         * synchronized with the main database file.
130         * <p>
131         * This method must be called while there are no
132         * pending transactions in the log.
133         */
134        public void setMaximumTransactionsInLog( int maxTxns )
135            throws IOException
136        {
137            if ( maxTxns <= 0 ) {
138                throw new IllegalArgumentException( I18n.err( I18n.ERR_563 ) );
139            }
140            if ( curTxn != -1 ) {
141                throw new IllegalStateException( I18n.err( I18n.ERR_564 ) );
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( I18n.err( I18n.ERR_565 ) );
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( I18n.err( I18n.ERR_566, 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    }