001/* 002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019 020package org.nuxeo.runtime.transaction; 021 022import java.lang.reflect.Field; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.List; 026import java.util.function.Supplier; 027 028import javax.naming.NamingException; 029import javax.transaction.HeuristicMixedException; 030import javax.transaction.HeuristicRollbackException; 031import javax.transaction.InvalidTransactionException; 032import javax.transaction.NotSupportedException; 033import javax.transaction.RollbackException; 034import javax.transaction.Status; 035import javax.transaction.Synchronization; 036import javax.transaction.SystemException; 037import javax.transaction.Transaction; 038import javax.transaction.TransactionManager; 039import javax.transaction.TransactionSynchronizationRegistry; 040import javax.transaction.UserTransaction; 041 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044import org.nuxeo.runtime.jtajca.NuxeoContainer; 045 046/** 047 * Utilities to work with transactions. 048 */ 049public class TransactionHelper { 050 051 private static final Log log = LogFactory.getLog(TransactionHelper.class); 052 053 private static final Field GERONIMO_TRANSACTION_TIMEOUT_FIELD; 054 static { 055 try { 056 GERONIMO_TRANSACTION_TIMEOUT_FIELD = org.apache.geronimo.transaction.manager.TransactionImpl.class.getDeclaredField( 057 "timeout"); 058 GERONIMO_TRANSACTION_TIMEOUT_FIELD.setAccessible(true); 059 } catch (NoSuchFieldException | SecurityException e) { 060 throw new ExceptionInInitializerError(e); 061 } 062 } 063 064 private TransactionHelper() { 065 // utility class 066 } 067 068 /** 069 * Looks up the User Transaction in JNDI. 070 * 071 * @return the User Transaction 072 * @throws NamingException if not found 073 */ 074 public static UserTransaction lookupUserTransaction() throws NamingException { 075 UserTransaction ut = NuxeoContainer.getUserTransaction(); 076 if (ut == null) { 077 throw new NamingException("tx manager not installed"); 078 } 079 return ut; 080 } 081 082 /** 083 * Gets the transaction status. 084 * 085 * @return the transaction {@linkplain Status status}, or -1 if there is no transaction manager 086 * @since 8.4 087 * @see Status 088 */ 089 public static int getTransactionStatus() { 090 UserTransaction ut = NuxeoContainer.getUserTransaction(); 091 if (ut == null) { 092 return -1; 093 } 094 try { 095 return ut.getStatus(); 096 } catch (SystemException e) { 097 throw new TransactionRuntimeException("Cannot get transaction status", e); 098 } 099 } 100 101 /** 102 * Returns the UserTransaction JNDI binding name. 103 * <p> 104 * Assumes {@link #lookupUserTransaction} has been called once before. 105 */ 106 public static String getUserTransactionJNDIName() { 107 return NuxeoContainer.nameOf("UserTransaction"); 108 } 109 110 /** 111 * Looks up the TransactionManager in JNDI. 112 * 113 * @return the TransactionManager 114 * @throws NamingException if not found 115 */ 116 public static TransactionManager lookupTransactionManager() throws NamingException { 117 TransactionManager tm = NuxeoContainer.getTransactionManager(); 118 if (tm == null) { 119 throw new NamingException("tx manager not installed"); 120 } 121 return tm; 122 } 123 124 /** 125 * Looks up the TransactionSynchronizationRegistry in JNDI. 126 * 127 * @return the TransactionSynchronizationRegistry 128 * @throws NamingException if not found 129 */ 130 public static TransactionSynchronizationRegistry lookupSynchronizationRegistry() throws NamingException { 131 TransactionSynchronizationRegistry synch = NuxeoContainer.getTransactionSynchronizationRegistry(); 132 if (synch == null) { 133 throw new NamingException("tx manager not installed"); 134 } 135 return synch; 136 } 137 138 /** 139 * Checks if there is no transaction 140 * 141 * @6.0 142 */ 143 public static boolean isNoTransaction() { 144 int status = getTransactionStatus(); 145 return status == Status.STATUS_NO_TRANSACTION || status == -1; 146 } 147 148 /** 149 * Checks if the current User Transaction is active. 150 */ 151 public static boolean isTransactionActive() { 152 int status = getTransactionStatus(); 153 return status == Status.STATUS_ACTIVE; 154 } 155 156 /** 157 * Checks if the current User Transaction is marked rollback only. 158 */ 159 public static boolean isTransactionMarkedRollback() { 160 int status = getTransactionStatus(); 161 return status == Status.STATUS_MARKED_ROLLBACK; 162 } 163 164 /** 165 * Checks if the current User Transaction is active or marked rollback only. 166 */ 167 public static boolean isTransactionActiveOrMarkedRollback() { 168 int status = getTransactionStatus(); 169 return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK; 170 } 171 172 /** 173 * Checks if the current User Transaction is active or preparing. 174 * 175 * @since 8.4 176 */ 177 public static boolean isTransactionActiveOrPreparing() { 178 int status = getTransactionStatus(); 179 return status == Status.STATUS_ACTIVE || status == Status.STATUS_PREPARING; 180 } 181 182 /** 183 * Checks if the current User Transaction has already timed out, i.e., whether a commit would immediately abort with 184 * a timeout exception. 185 * 186 * @return {@code true} if there is a current transaction that has timed out, {@code false} otherwise 187 * @since 7.1 188 */ 189 public static boolean isTransactionTimedOut() { 190 TransactionManager tm = NuxeoContainer.getTransactionManager(); 191 if (tm == null) { 192 return false; 193 } 194 try { 195 Transaction tx = tm.getTransaction(); 196 if (tx == null || tx.getStatus() != Status.STATUS_ACTIVE) { 197 return false; 198 } 199 if (tx instanceof org.apache.geronimo.transaction.manager.TransactionImpl) { 200 // Geronimo Transaction Manager 201 Long timeout = (Long) GERONIMO_TRANSACTION_TIMEOUT_FIELD.get(tx); 202 return System.currentTimeMillis() > timeout.longValue(); 203 } else { 204 // unknown transaction manager 205 return false; 206 } 207 } catch (SystemException | ReflectiveOperationException e) { 208 throw new RuntimeException(e); 209 } 210 } 211 212 /** 213 * Checks if the current User Transaction has already timed out, i.e., whether a commit would immediately abort with 214 * a timeout exception. 215 * <p> 216 * Throws if the transaction has timed out. 217 * 218 * @throws TransactionRuntimeException if the transaction has timed out 219 * @since 8.4 220 */ 221 public static void checkTransactionTimeout() throws TransactionRuntimeException { 222 if (isTransactionTimedOut()) { 223 throw new TransactionRuntimeException("Transaction has timed out"); 224 } 225 } 226 227 /** 228 * Starts a new User Transaction. 229 * 230 * @return {@code true} if the transaction was successfully started, {@code false} otherwise 231 */ 232 public static boolean startTransaction() { 233 UserTransaction ut = NuxeoContainer.getUserTransaction(); 234 if (ut == null) { 235 return false; 236 } 237 try { 238 if (log.isDebugEnabled()) { 239 log.debug("Starting transaction"); 240 } 241 ut.begin(); 242 return true; 243 } catch (NotSupportedException | SystemException e) { 244 log.error("Unable to start transaction", e); 245 } 246 return false; 247 } 248 249 /** 250 * Suspend the current transaction if active and start a new transaction 251 * 252 * @return the suspended transaction or null 253 * @throws TransactionRuntimeException 254 * @since 5.6 255 */ 256 public static Transaction requireNewTransaction() { 257 TransactionManager tm = NuxeoContainer.getTransactionManager(); 258 if (tm == null) { 259 return null; 260 } 261 try { 262 Transaction tx = tm.getTransaction(); 263 if (tx != null) { 264 tx = tm.suspend(); 265 } 266 tm.begin(); 267 return tx; 268 } catch (NotSupportedException | SystemException e) { 269 throw new TransactionRuntimeException("Cannot suspend tx", e); 270 } 271 } 272 273 public static Transaction suspendTransaction() { 274 TransactionManager tm = NuxeoContainer.getTransactionManager(); 275 if (tm == null) { 276 return null; 277 } 278 try { 279 Transaction tx = tm.getTransaction(); 280 if (tx != null) { 281 tx = tm.suspend(); 282 } 283 return tx; 284 } catch (SystemException e) { 285 throw new TransactionRuntimeException("Cannot suspend tx", e); 286 } 287 } 288 289 /** 290 * Commit the current transaction if active and resume the principal transaction 291 * 292 * @param tx 293 */ 294 public static void resumeTransaction(Transaction tx) { 295 TransactionManager tm = NuxeoContainer.getTransactionManager(); 296 if (tm == null) { 297 return; 298 } 299 try { 300 if (tm.getStatus() == Status.STATUS_ACTIVE) { 301 tm.commit(); 302 } 303 if (tx != null) { 304 tm.resume(tx); 305 } 306 } catch (SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException 307 | InvalidTransactionException | IllegalStateException | SecurityException e) { 308 throw new TransactionRuntimeException("Cannot resume tx", e); 309 } 310 } 311 312 /** 313 * Starts a new User Transaction with the specified timeout. 314 * 315 * @param timeout the timeout in seconds, <= 0 for the default 316 * @return {@code true} if the transaction was successfully started, {@code false} otherwise 317 * @since 5.6 318 */ 319 public static boolean startTransaction(int timeout) { 320 if (timeout < 0) { 321 timeout = 0; 322 } 323 TransactionManager tm = NuxeoContainer.getTransactionManager(); 324 if (tm == null) { 325 return false; 326 } 327 328 try { 329 tm.setTransactionTimeout(timeout); 330 } catch (SystemException e) { 331 log.error("Unable to set transaction timeout: " + timeout, e); 332 return false; 333 } 334 try { 335 return startTransaction(); 336 } finally { 337 try { 338 tm.setTransactionTimeout(0); 339 } catch (SystemException e) { 340 log.error("Unable to reset transaction timeout", e); 341 } 342 } 343 } 344 345 /** 346 * Commits or rolls back the User Transaction depending on the transaction status. 347 */ 348 public static void commitOrRollbackTransaction() { 349 UserTransaction ut = NuxeoContainer.getUserTransaction(); 350 if (ut == null) { 351 return; 352 } 353 try { 354 int status = ut.getStatus(); 355 if (status == Status.STATUS_ACTIVE) { 356 if (log.isDebugEnabled()) { 357 log.debug("Committing transaction"); 358 } 359 try { 360 ut.commit(); 361 } catch (RollbackException | HeuristicRollbackException | HeuristicMixedException e) { 362 String msg = "Unable to commit"; 363 // messages from org.apache.geronimo.transaction.manager.TransactionImpl.commit 364 switch (e.getMessage()) { 365 case "Unable to commit: transaction marked for rollback": 366 // don't log as error, this happens if there's a ConcurrentUpdateException 367 // at transaction end inside VCS 368 case "Unable to commit: Transaction timeout": 369 // don't log either 370 log.debug(msg, e); 371 break; 372 default: 373 log.error(msg, e); 374 } 375 throw new TransactionRuntimeException(e.getMessage(), e); 376 } 377 } else if (status == Status.STATUS_MARKED_ROLLBACK) { 378 if (log.isDebugEnabled()) { 379 log.debug("Cannot commit transaction because it is marked rollback only"); 380 } 381 ut.rollback(); 382 } else { 383 if (log.isDebugEnabled()) { 384 log.debug("Cannot commit transaction with unknown status: " + status); 385 } 386 } 387 } catch (SystemException | IllegalStateException | SecurityException e) { 388 String msg = "Unable to commit/rollback"; 389 log.error(msg, e); 390 throw new TransactionRuntimeException(msg + ": " + e.getMessage(), e); 391 } 392 } 393 394 private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>(); 395 396 /** 397 * After this, some exceptions during transaction commit may be suppressed and remembered. 398 * 399 * @since 5.9.4 400 */ 401 public static void noteSuppressedExceptions() { 402 suppressedExceptions.set(new ArrayList<Exception>(1)); 403 } 404 405 /** 406 * If activated by {@linked #noteSuppressedExceptions}, remembers the exception. 407 * 408 * @since 5.9.4 409 */ 410 public static void noteSuppressedException(Exception e) { 411 List<Exception> exceptions = suppressedExceptions.get(); 412 if (exceptions != null) { 413 exceptions.add(e); 414 } 415 } 416 417 /** 418 * Gets the suppressed exceptions, and stops remembering. 419 * 420 * @since 5.9.4 421 */ 422 public static List<Exception> getSuppressedExceptions() { 423 List<Exception> exceptions = suppressedExceptions.get(); 424 suppressedExceptions.remove(); 425 return exceptions == null ? Collections.<Exception> emptyList() : exceptions; 426 } 427 428 /** 429 * Sets the current User Transaction as rollback only. 430 * 431 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 432 */ 433 public static boolean setTransactionRollbackOnly() { 434 if (log.isDebugEnabled()) { 435 log.debug("Setting transaction as rollback only"); 436 if (log.isTraceEnabled()) { 437 log.trace("Rollback stack trace", new Throwable("Rollback stack trace")); 438 } 439 } 440 UserTransaction ut = NuxeoContainer.getUserTransaction(); 441 if (ut == null) { 442 return false; 443 } 444 try { 445 ut.setRollbackOnly(); 446 return true; 447 } catch (IllegalStateException | SystemException cause) { 448 log.error("Could not mark transaction as rollback only", cause); 449 } 450 return false; 451 } 452 453 /** 454 * Sets the current User Transaction as rollback only if it has timed out. 455 * 456 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 457 * @since 7.1 458 */ 459 public static boolean setTransactionRollbackOnlyIfTimedOut() { 460 if (isTransactionTimedOut()) { 461 return setTransactionRollbackOnly(); 462 } 463 return false; 464 } 465 466 public static void registerSynchronization(Synchronization handler) { 467 if (!isTransactionActiveOrPreparing()) { 468 throw new TransactionRuntimeException("Cannot register Synchronization if transaction is not active"); 469 } 470 try { 471 NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler); 472 } catch (IllegalStateException | RollbackException | SystemException cause) { 473 throw new RuntimeException("Cannot register synch handler in current tx", cause); 474 } 475 } 476 477 /** 478 * Runs the given {@link Runnable} without a transactional context. Will suspend and restore the transaction if one already 479 * exists. 480 * 481 * @param runnable the {@link Runnable} 482 * @since 9.1 483 */ 484 public static void runWithoutTransaction(Runnable runnable) { 485 runWithoutTransaction(() -> { runnable.run(); return null; }); 486 } 487 488 489 /** 490 * Calls the given {@link Supplier} without a transactional context. Will suspend and restore the transaction if one already 491 * exists. 492 * 493 * @param supplier the {@link Supplier} 494 * @since 9.1 495 */ 496 public static <R> R runWithoutTransaction(Supplier<R> supplier) { 497 Transaction tx = suspendTransaction(); 498 try { 499 return supplier.get(); 500 } finally { 501 resumeTransaction(tx); 502 } 503 } 504 505 /** 506 * Runs the given {@link Runnable} in a new transactional context. Will suspend and restore the transaction if one already 507 * exists. 508 * 509 * @param runnable the {@link Runnable} 510 * @since 9.1 511 */ 512 public static void runInNewTransaction(Runnable runnable) { 513 runInNewTransaction(() -> { runnable.run(); return null;}); 514 } 515 516 /** 517 * Calls the given {@link Supplier} in a new transactional context. Will suspend and restore the transaction if one already 518 * exists. 519 * 520 * @param supplier the {@link Supplier} 521 * @since 9.1 522 */ 523 public static <R> R runInNewTransaction(Supplier<R> supplier) { 524 Transaction tx = suspendTransaction(); 525 try { 526 return runInTransaction(supplier); 527 } finally { 528 resumeTransaction(tx); 529 } 530 } 531 532 /** 533 * Runs the given {@link Runnable} in a transactional context. Will not start a new transaction if one already 534 * exists. 535 * 536 * @param runnable the {@link Runnable} 537 * @since 8.4 538 */ 539 public static void runInTransaction(Runnable runnable) { 540 runInTransaction(() -> {runnable.run(); return null;}); 541 } 542 543 /** 544 * Calls the given {@link Supplier} in a transactional context. Will not start a new transaction if one already 545 * exists. 546 * 547 * @param supplier the {@link Supplier} 548 * @return the supplier's result 549 * @since 8.4 550 */ 551 public static <R> R runInTransaction(Supplier<R> supplier) { 552 boolean startTransaction = !isTransactionActiveOrMarkedRollback(); 553 if (startTransaction) { 554 if (!startTransaction()) { 555 throw new TransactionRuntimeException("Cannot start transaction"); 556 } 557 } 558 boolean completedAbruptly = true; 559 try { 560 R result = supplier.get(); 561 completedAbruptly = false; 562 return result; 563 } finally { 564 try { 565 if (completedAbruptly) { 566 setTransactionRollbackOnly(); 567 } 568 } finally { 569 if (startTransaction) { 570 commitOrRollbackTransaction(); 571 } 572 } 573 } 574 } 575 576}