1/*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2000-2009 Oracle. All rights reserved. 5 * 6 * $Id$ 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, 315 CursorConfig cursorConfig, 316 boolean writeCursor, 317 Transaction txn) 318 throws DatabaseException { 319 320 if (cdbMode) { 321 CdbCursors cdbCursors = null; 322 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 323 if (cdbCursorsMap == null) { 324 cdbCursorsMap = new WeakHashMap(); 325 localCdbCursors.set(cdbCursorsMap); 326 } else { 327 cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 328 } 329 if (cdbCursors == null) { 330 cdbCursors = new CdbCursors(); 331 cdbCursorsMap.put(db, cdbCursors); 332 } 333 334 /* 335 * In CDB mode the cursorConfig specified by the user is ignored 336 * and only the writeCursor parameter is honored. This is the only 337 * meaningful cursor attribute for CDB, and here we count on 338 * writeCursor flag being set correctly by the caller. 339 */ 340 List cursors; 341 CursorConfig cdbConfig; 342 if (writeCursor) { 343 if (cdbCursors.readCursors.size() > 0) { 344 345 /* 346 * Although CDB allows opening a write cursor when a read 347 * cursor is open, a self-deadlock will occur if a write is 348 * attempted for a record that is read-locked; we should 349 * avoid self-deadlocks at all costs 350 */ 351 throw new IllegalStateException( 352 "cannot open CDB write cursor when read cursor is open"); 353 } 354 cursors = cdbCursors.writeCursors; 355 cdbConfig = new CursorConfig(); 356 DbCompat.setWriteCursor(cdbConfig, true); 357 } else { 358 cursors = cdbCursors.readCursors; 359 cdbConfig = null; 360 } 361 Cursor cursor; 362 if (cursors.size() > 0) { 363 Cursor other = ((Cursor) cursors.get(0)); 364 cursor = other.dup(false); 365 } else { 366 cursor = db.openCursor(null, cdbConfig); 367 } 368 cursors.add(cursor); 369 return cursor; 370 } else { 371 return db.openCursor(txn, cursorConfig); 372 } 373 } 374 375 /** 376 * Duplicates a cursor for a given database. 377 * 378 * @param writeCursor true to open a write cursor in a CDB environment, and 379 * ignored for other environments. 380 * 381 * @param samePosition is passed through to Cursor.dup(). 382 * 383 * @return the open cursor. 384 * 385 * @throws DatabaseException if a database problem occurs. 386 */ 387 Cursor dupCursor(Cursor cursor, boolean writeCursor, boolean samePosition) 388 throws DatabaseException { 389 390 if (cdbMode) { 391 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 392 if (cdbCursorsMap != null) { 393 Database db = cursor.getDatabase(); 394 CdbCursors cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 395 if (cdbCursors != null) { 396 List cursors = writeCursor ? cdbCursors.writeCursors 397 : cdbCursors.readCursors; 398 if (cursors.contains(cursor)) { 399 Cursor newCursor = cursor.dup(samePosition); 400 cursors.add(newCursor); 401 return newCursor; 402 } 403 } 404 } 405 throw new IllegalStateException("cursor to dup not tracked"); 406 } else { 407 return cursor.dup(samePosition); 408 } 409 } 410 411 /** 412 * Closes a cursor. 413 * 414 * @param cursor the cursor to close. 415 * 416 * @throws DatabaseException if a database problem occurs. 417 */ 418 void closeCursor(Cursor cursor) 419 throws DatabaseException { 420 421 if (cursor == null) { 422 return; 423 } 424 if (cdbMode) { 425 WeakHashMap cdbCursorsMap = (WeakHashMap) localCdbCursors.get(); 426 if (cdbCursorsMap != null) { 427 Database db = cursor.getDatabase(); 428 CdbCursors cdbCursors = (CdbCursors) cdbCursorsMap.get(db); 429 if (cdbCursors != null) { 430 if (cdbCursors.readCursors.remove(cursor) || 431 cdbCursors.writeCursors.remove(cursor)) { 432 cursor.close(); 433 return; 434 } 435 } 436 } 437 throw new IllegalStateException( 438 "closing CDB cursor that was not known to be open"); 439 } else { 440 cursor.close(); 441 } 442 } 443 444 /** 445 * Returns true if a CDB cursor is open and therefore a Database write 446 * operation should not be attempted since a self-deadlock may result. 447 */ 448 boolean isCDBCursorOpen(Database db) { 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