1/*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2000,2008 Oracle. All rights reserved. 5 * 6 * $Id: CurrentTransaction.java,v 12.11 2008/02/07 17:12:26 mark Exp $ 7 */ 8 9package com.sleepycat.collections; 10 11import java.lang.ref.WeakReference; 12import java.util.ArrayList; 13import java.util.List; 14import java.util.WeakHashMap; 15 16import com.sleepycat.compat.DbCompat; 17import com.sleepycat.db.Cursor; 18import com.sleepycat.db.CursorConfig; 19import com.sleepycat.db.Database; 20import com.sleepycat.db.DatabaseException; 21import com.sleepycat.db.Environment; 22import com.sleepycat.db.EnvironmentConfig; 23import com.sleepycat.db.LockMode; 24import com.sleepycat.db.Transaction; 25import com.sleepycat.db.TransactionConfig; 26import com.sleepycat.util.RuntimeExceptionWrapper; 27 28/** 29 * Provides access to the current transaction for the current thread within the 30 * context of a Berkeley DB environment. This class provides explicit 31 * transaction control beyond that provided by the {@link TransactionRunner} 32 * class. However, both methods of transaction control manage per-thread 33 * transactions. 34 * 35 * @author Mark Hayes 36 */ 37public class CurrentTransaction { 38 39 /* For internal use, this class doubles as an Environment wrapper. */ 40 41 private static WeakHashMap<Environment,CurrentTransaction> envMap = 42 new WeakHashMap<Environment,CurrentTransaction>(); 43 44 private LockMode writeLockMode; 45 private boolean cdbMode; 46 private boolean txnMode; 47 private boolean lockingMode; 48 private ThreadLocal localTrans = new ThreadLocal(); 49 private ThreadLocal localCdbCursors; 50 51 /* 52 * Use a WeakReference to the Environment to avoid pinning the environment 53 * in the envMap. The WeakHashMap envMap uses the Environment as a weak 54 * key, but this won't prevent GC of the Environment if the map's value has 55 * a hard reference to the Environment. [#15444] 56 */ 57 private WeakReference<Environment> envRef; 58 59 /** 60 * Gets the CurrentTransaction accessor for a specified Berkeley DB 61 * environment. This method always returns the same reference when called 62 * more than once with the same environment parameter. 63 * 64 * @param env is an open Berkeley DB environment. 65 * 66 * @return the CurrentTransaction accessor for the given environment, or 67 * null if the environment is not transactional. 68 */ 69 public static CurrentTransaction getInstance(Environment env) { 70 71 CurrentTransaction currentTxn = getInstanceInternal(env); 72 return currentTxn.isTxnMode() ? currentTxn : null; 73 } 74 75 /** 76 * Gets the CurrentTransaction accessor for a specified Berkeley DB 77 * environment. Unlike getInstance(), this method never returns null. 78 * 79 * @param env is an open Berkeley DB environment. 80 */ 81 static CurrentTransaction getInstanceInternal(Environment env) { 82 synchronized (envMap) { 83 CurrentTransaction ct = envMap.get(env); 84 if (ct == null) { 85 ct = new CurrentTransaction(env); 86 envMap.put(env, ct); 87 } 88 return ct; 89 } 90 } 91 92 private CurrentTransaction(Environment env) { 93 envRef = new WeakReference<Environment>(env); 94 try { 95 EnvironmentConfig config = env.getConfig(); 96 txnMode = config.getTransactional(); 97 lockingMode = DbCompat.getInitializeLocking(config); 98 if (txnMode || lockingMode) { 99 writeLockMode = LockMode.RMW; 100 } else { 101 writeLockMode = LockMode.DEFAULT; 102 } 103 cdbMode = DbCompat.getInitializeCDB(config); 104 if (cdbMode) { 105 localCdbCursors = new ThreadLocal(); 106 } 107 } catch (DatabaseException e) { 108 throw new RuntimeExceptionWrapper(e); 109 } 110 } 111 112 /** 113 * Returns whether environment is configured for locking. 114 */ 115 final boolean isLockingMode() { 116 117 return lockingMode; 118 } 119 120 /** 121 * Returns whether this is a transactional environment. 122 */ 123 final boolean isTxnMode() { 124 125 return txnMode; 126 } 127 128 /** 129 * Returns whether this is a Concurrent Data Store environment. 130 */ 131 final boolean isCdbMode() { 132 133 return cdbMode; 134 } 135 136 /** 137 * Return the LockMode.RMW or null, depending on whether locking is 138 * enabled. LockMode.RMW will cause an error if passed when locking 139 * is not enabled. Locking is enabled if locking or transactions were 140 * specified for this environment. 141 */ 142 final LockMode getWriteLockMode() { 143 144 return writeLockMode; 145 } 146 147 /** 148 * Returns the underlying Berkeley DB environment. 149 */ 150 public final Environment getEnvironment() { 151 152 return envRef.get(); 153 } 154 155 /** 156 * Returns the transaction associated with the current thread for this 157 * environment, or null if no transaction is active. 158 */ 159 public final Transaction getTransaction() { 160 161 Trans trans = (Trans) localTrans.get(); 162 return (trans != null) ? trans.txn : null; 163 } 164 165 /** 166 * Returns whether auto-commit may be performed by the collections API. 167 * True is returned if no collections API transaction is currently active, 168 * and no XA transaction is currently active. 169 */ 170 boolean isAutoCommitAllowed() 171 throws DatabaseException { 172 173 return getTransaction() == null && 174 DbCompat.getThreadTransaction(getEnvironment()) == null; 175 } 176 177 /** 178 * Begins a new transaction for this environment and associates it with 179 * the current thread. If a transaction is already active for this 180 * environment and thread, a nested transaction will be created. 181 * 182 * @param config the transaction configuration used for calling 183 * {@link Environment#beginTransaction}, or null to use the default 184 * configuration. 185 * 186 * @return the new transaction. 187 * 188 * @throws DatabaseException if the transaction cannot be started, in which 189 * case any existing transaction is not affected. 190 * 191 * @throws IllegalStateException if a transaction is already active and 192 * nested transactions are not supported by the environment. 193 */ 194 public final Transaction beginTransaction(TransactionConfig config) 195 throws DatabaseException { 196 197 Environment env = getEnvironment(); 198 Trans trans = (Trans) localTrans.get(); 199 if (trans != null) { 200 if (trans.txn != null) { 201 if (!DbCompat.NESTED_TRANSACTIONS) { 202 throw new IllegalStateException( 203 "Nested transactions are not supported"); 204 } 205 Transaction parentTxn = trans.txn; 206 trans = new Trans(trans, config); 207 trans.txn = env.beginTransaction(parentTxn, config); 208 localTrans.set(trans); 209 } else { 210 trans.txn = env.beginTransaction(null, config); 211 trans.config = config; 212 } 213 } else { 214 trans = new Trans(null, config); 215 trans.txn = env.beginTransaction(null, config); 216 localTrans.set(trans); 217 } 218 return trans.txn; 219 } 220 221 /** 222 * Commits the transaction that is active for the current thread for this 223 * environment and makes the parent transaction (if any) the current 224 * transaction. 225 * 226 * @return the parent transaction or null if the committed transaction was 227 * not nested. 228 * 229 * @throws DatabaseException if an error occurs committing the transaction. 230 * The transaction will still be closed and the parent transaction will 231 * become the current transaction. 232 * 233 * @throws IllegalStateException if no transaction is active for the 234 * current thread for this environment. 235 */ 236 public final Transaction commitTransaction() 237 throws DatabaseException, IllegalStateException { 238 239 Trans trans = (Trans) localTrans.get(); 240 if (trans != null && trans.txn != null) { 241 Transaction parent = closeTxn(trans); 242 trans.txn.commit(); 243 return parent; 244 } else { 245 throw new IllegalStateException("No transaction is active"); 246 } 247 } 248 249 /** 250 * Aborts the transaction that is active for the current thread for this 251 * environment and makes the parent transaction (if any) the current 252 * transaction. 253 * 254 * @return the parent transaction or null if the aborted transaction was 255 * not nested. 256 * 257 * @throws DatabaseException if an error occurs aborting the transaction. 258 * The transaction will still be closed and the parent transaction will 259 * become the current transaction. 260 * 261 * @throws IllegalStateException if no transaction is active for the 262 * current thread for this environment. 263 */ 264 public final Transaction abortTransaction() 265 throws DatabaseException, IllegalStateException { 266 267 Trans trans = (Trans) localTrans.get(); 268 if (trans != null && trans.txn != null) { 269 Transaction parent = closeTxn(trans); 270 trans.txn.abort(); 271 return parent; 272 } else { 273 throw new IllegalStateException("No transaction is active"); 274 } 275 } 276 277 /** 278 * Returns whether the current transaction is a readUncommitted 279 * transaction. 280 */ 281 final boolean isReadUncommitted() { 282 283 Trans trans = (Trans) localTrans.get(); 284 if (trans != null && trans.config != null) { 285 return trans.config.getReadUncommitted(); 286 } else { 287 return false; 288 } 289 } 290 291 private Transaction closeTxn(Trans trans) { 292 293 localTrans.set(trans.parent); 294 return (trans.parent != null) ? trans.parent.txn : null; 295 } 296 297 private static class Trans { 298 299 private Trans parent; 300 private Transaction txn; 301 private TransactionConfig config; 302 303 private Trans(Trans parent, TransactionConfig config) { 304 305 this.parent = parent; 306 this.config = config; 307 } 308 } 309 310 /** 311 * Opens a cursor for a given database, dup'ing an existing CDB cursor if 312 * one is open for the current thread. 313 */ 314 Cursor openCursor(Database db, CursorConfig cursorConfig, 315 boolean writeCursor, Transaction txn) 316 throws DatabaseException { 317 318 if (cdbMode) { 319 CdbCursors cdbCursors = null; 320 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 321 if (cdbCursorsMap == null) { 322 cdbCursorsMap = new WeakHashMap(); 323 localCdbCursors.set(cdbCursorsMap); 324 } else { 325 cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 326 } 327 if (cdbCursors == null) { 328 cdbCursors = new CdbCursors(); 329 cdbCursorsMap.put(db, cdbCursors); 330 } 331 332 /* 333 * In CDB mode the cursorConfig specified by the user is ignored 334 * and only the writeCursor parameter is honored. This is the only 335 * meaningful cursor attribute for CDB, and here we count on 336 * writeCursor flag being set correctly by the caller. 337 */ 338 List cursors; 339 CursorConfig cdbConfig; 340 if (writeCursor) { 341 if (cdbCursors.readCursors.size() > 0) { 342 343 /* 344 * Although CDB allows opening a write cursor when a read 345 * cursor is open, a self-deadlock will occur if a write is 346 * attempted for a record that is read-locked; we should 347 * avoid self-deadlocks at all costs 348 */ 349 throw new IllegalStateException( 350 "cannot open CDB write cursor when read cursor is open"); 351 } 352 cursors = cdbCursors.writeCursors; 353 cdbConfig = new CursorConfig(); 354 DbCompat.setWriteCursor(cdbConfig, true); 355 } else { 356 cursors = cdbCursors.readCursors; 357 cdbConfig = null; 358 } 359 Cursor cursor; 360 if (cursors.size() > 0) { 361 Cursor other = ((Cursor) cursors.get(0)); 362 cursor = other.dup(false); 363 } else { 364 cursor = db.openCursor(null, cdbConfig); 365 } 366 cursors.add(cursor); 367 return cursor; 368 } else { 369 return db.openCursor(txn, cursorConfig); 370 } 371 } 372 373 /** 374 * Duplicates a cursor for a given database. 375 * 376 * @param writeCursor true to open a write cursor in a CDB environment, and 377 * ignored for other environments. 378 * 379 * @param samePosition is passed through to Cursor.dup(). 380 * 381 * @return the open cursor. 382 * 383 * @throws DatabaseException if a database problem occurs. 384 */ 385 Cursor dupCursor(Cursor cursor, boolean writeCursor, boolean samePosition) 386 throws DatabaseException { 387 388 if (cdbMode) { 389 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 390 if (cdbCursorsMap != null) { 391 Database db = cursor.getDatabase(); 392 CdbCursors cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 393 if (cdbCursors != null) { 394 List cursors = writeCursor ? cdbCursors.writeCursors 395 : cdbCursors.readCursors; 396 if (cursors.contains(cursor)) { 397 Cursor newCursor = cursor.dup(samePosition); 398 cursors.add(newCursor); 399 return newCursor; 400 } 401 } 402 } 403 throw new IllegalStateException("cursor to dup not tracked"); 404 } else { 405 return cursor.dup(samePosition); 406 } 407 } 408 409 /** 410 * Closes a cursor. 411 * 412 * @param cursor the cursor to close. 413 * 414 * @throws DatabaseException if a database problem occurs. 415 */ 416 void closeCursor(Cursor cursor) 417 throws DatabaseException { 418 419 if (cursor == null) { 420 return; 421 } 422 if (cdbMode) { 423 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 424 if (cdbCursorsMap != null) { 425 Database db = cursor.getDatabase(); 426 CdbCursors cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 427 if (cdbCursors != null) { 428 if (cdbCursors.readCursors.remove(cursor) || 429 cdbCursors.writeCursors.remove(cursor)) { 430 cursor.close(); 431 return; 432 } 433 } 434 } 435 throw new IllegalStateException( 436 "closing CDB cursor that was not known to be open"); 437 } else { 438 cursor.close(); 439 } 440 } 441 442 /** 443 * Returns true if a CDB cursor is open and therefore a Database write 444 * operation should not be attempted since a self-deadlock may result. 445 */ 446 boolean isCDBCursorOpen(Database db) 447 throws DatabaseException { 448 449 if (cdbMode) { 450 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 451 if (cdbCursorsMap != null) { 452 CdbCursors cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 453 454 if (cdbCursors != null && 455 (cdbCursors.readCursors.size() > 0 || 456 cdbCursors.writeCursors.size() > 0)) { 457 return true; 458 } 459 } 460 } 461 return false; 462 } 463 464 static final class CdbCursors { 465 466 List writeCursors = new ArrayList(); 467 List readCursors = new ArrayList(); 468 } 469} 470