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 noteSuppressedExceptions(); 354 RuntimeException thrown = null; 355 boolean isRollbackDuringCommit = false; 356 try { 357 int status = ut.getStatus(); 358 if (status == Status.STATUS_ACTIVE) { 359 if (log.isDebugEnabled()) { 360 log.debug("Committing transaction"); 361 } 362 try { 363 ut.commit(); 364 } catch (RollbackException | HeuristicRollbackException | HeuristicMixedException e) { 365 String msg = "Unable to commit"; 366 // messages from org.apache.geronimo.transaction.manager.TransactionImpl.commit 367 switch (e.getMessage()) { 368 case "Unable to commit: transaction marked for rollback": 369 // don't log as error, this happens if there's a ConcurrentUpdateException 370 // at transaction end inside VCS 371 isRollbackDuringCommit = true; 372 // $FALL-THROUGH$ 373 case "Unable to commit: Transaction timeout": 374 // don't log either 375 log.debug(msg, e); 376 break; 377 default: 378 log.error(msg, e); 379 } 380 throw new TransactionRuntimeException(e.getMessage(), e); 381 } 382 } else if (status == Status.STATUS_MARKED_ROLLBACK) { 383 if (log.isDebugEnabled()) { 384 log.debug("Cannot commit transaction because it is marked rollback only"); 385 } 386 ut.rollback(); 387 } else { 388 if (log.isDebugEnabled()) { 389 log.debug("Cannot commit transaction with unknown status: " + status); 390 } 391 } 392 } catch (SystemException e) { 393 thrown = new TransactionRuntimeException(e); 394 throw thrown; 395 } catch (RuntimeException e) { 396 thrown = e; 397 throw thrown; 398 } finally { 399 List<Exception> suppressed = getSuppressedExceptions(); 400 if (!suppressed.isEmpty()) { 401 // add suppressed to thrown exception, or throw a new one 402 RuntimeException e; 403 if (thrown == null) { 404 e = new TransactionRuntimeException("Exception during commit"); 405 } else { 406 if (isRollbackDuringCommit && suppressed.get(0) instanceof RuntimeException) { 407 // use the suppressed one directly and throw it instead 408 thrown = null; // force rethrow below 409 e = (RuntimeException) suppressed.remove(0); 410 } else { 411 e = thrown; 412 } 413 } 414 suppressed.forEach(s -> e.addSuppressed(s)); 415 if (thrown == null) { 416 throw e; 417 } 418 } 419 } 420 } 421 422 private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>(); 423 424 /** 425 * After this, some exceptions during transaction commit may be suppressed and remembered. 426 * 427 * @since 5.9.4 428 */ 429 protected static void noteSuppressedExceptions() { 430 suppressedExceptions.set(new ArrayList<>()); 431 } 432 433 /** 434 * Remembers the exception if it happens during the processing of a commit, so that it can be surfaced as a 435 * suppressed exception at the end of the commit. 436 * 437 * @since 5.9.4 438 */ 439 public static void noteSuppressedException(Exception e) { 440 List<Exception> exceptions = suppressedExceptions.get(); 441 if (exceptions != null) { 442 exceptions.add(e); 443 } 444 } 445 446 /** 447 * Gets the suppressed exceptions, and stops remembering. 448 * 449 * @since 5.9.4 450 */ 451 protected static List<Exception> getSuppressedExceptions() { 452 List<Exception> exceptions = suppressedExceptions.get(); 453 suppressedExceptions.remove(); 454 return exceptions == null ? Collections.<Exception> emptyList() : exceptions; 455 } 456 457 /** 458 * Sets the current User Transaction as rollback only. 459 * 460 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 461 */ 462 public static boolean setTransactionRollbackOnly() { 463 if (log.isDebugEnabled()) { 464 log.debug("Setting transaction as rollback only"); 465 if (log.isTraceEnabled()) { 466 log.trace("Rollback stack trace", new Throwable("Rollback stack trace")); 467 } 468 } 469 UserTransaction ut = NuxeoContainer.getUserTransaction(); 470 if (ut == null) { 471 return false; 472 } 473 try { 474 ut.setRollbackOnly(); 475 return true; 476 } catch (IllegalStateException | SystemException cause) { 477 log.error("Could not mark transaction as rollback only", cause); 478 } 479 return false; 480 } 481 482 /** 483 * Sets the current User Transaction as rollback only if it has timed out. 484 * 485 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 486 * @since 7.1 487 */ 488 public static boolean setTransactionRollbackOnlyIfTimedOut() { 489 if (isTransactionTimedOut()) { 490 return setTransactionRollbackOnly(); 491 } 492 return false; 493 } 494 495 public static void registerSynchronization(Synchronization handler) { 496 if (!isTransactionActiveOrPreparing()) { 497 throw new TransactionRuntimeException("Cannot register Synchronization if transaction is not active"); 498 } 499 try { 500 NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler); 501 } catch (IllegalStateException | RollbackException | SystemException cause) { 502 throw new RuntimeException("Cannot register synch handler in current tx", cause); 503 } 504 } 505 506 /** 507 * Runs the given {@link Runnable} without a transactional context. Will suspend and restore the transaction if one already 508 * exists. 509 * 510 * @param runnable the {@link Runnable} 511 * @since 9.1 512 */ 513 public static void runWithoutTransaction(Runnable runnable) { 514 runWithoutTransaction(() -> { runnable.run(); return null; }); 515 } 516 517 518 /** 519 * Calls the given {@link Supplier} without a transactional context. Will suspend and restore the transaction if one already 520 * exists. 521 * 522 * @param supplier the {@link Supplier} 523 * @since 9.1 524 */ 525 public static <R> R runWithoutTransaction(Supplier<R> supplier) { 526 Transaction tx = suspendTransaction(); 527 try { 528 return supplier.get(); 529 } finally { 530 resumeTransaction(tx); 531 } 532 } 533 534 /** 535 * Runs the given {@link Runnable} in a new transactional context. Will suspend and restore the transaction if one already 536 * exists. 537 * 538 * @param runnable the {@link Runnable} 539 * @since 9.1 540 */ 541 public static void runInNewTransaction(Runnable runnable) { 542 runInNewTransaction(() -> { runnable.run(); return null;}); 543 } 544 545 /** 546 * Calls the given {@link Supplier} in a new transactional context. Will suspend and restore the transaction if one already 547 * exists. 548 * 549 * @param supplier the {@link Supplier} 550 * @since 9.1 551 */ 552 public static <R> R runInNewTransaction(Supplier<R> supplier) { 553 Transaction tx = suspendTransaction(); 554 try { 555 return runInTransaction(supplier); 556 } finally { 557 resumeTransaction(tx); 558 } 559 } 560 561 /** 562 * Runs the given {@link Runnable} in a transactional context. Will not start a new transaction if one already 563 * exists. 564 * 565 * @param runnable the {@link Runnable} 566 * @since 8.4 567 */ 568 public static void runInTransaction(Runnable runnable) { 569 runInTransaction(() -> {runnable.run(); return null;}); 570 } 571 572 /** 573 * Calls the given {@link Supplier} in a transactional context. Will not start a new transaction if one already 574 * exists. 575 * 576 * @param supplier the {@link Supplier} 577 * @return the supplier's result 578 * @since 8.4 579 */ 580 public static <R> R runInTransaction(Supplier<R> supplier) { 581 boolean startTransaction = !isTransactionActiveOrMarkedRollback(); 582 if (startTransaction) { 583 if (!startTransaction()) { 584 throw new TransactionRuntimeException("Cannot start transaction"); 585 } 586 } 587 boolean completedAbruptly = true; 588 try { 589 R result = supplier.get(); 590 completedAbruptly = false; 591 return result; 592 } finally { 593 try { 594 if (completedAbruptly) { 595 setTransactionRollbackOnly(); 596 } 597 } finally { 598 if (startTransaction) { 599 commitOrRollbackTransaction(); 600 } 601 } 602 } 603 } 604 605}