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 }