001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012 013package org.nuxeo.runtime.transaction; 014 015import java.lang.reflect.Field; 016import java.util.ArrayList; 017import java.util.Collections; 018import java.util.List; 019 020import javax.naming.NamingException; 021import javax.transaction.HeuristicMixedException; 022import javax.transaction.HeuristicRollbackException; 023import javax.transaction.InvalidTransactionException; 024import javax.transaction.NotSupportedException; 025import javax.transaction.RollbackException; 026import javax.transaction.Status; 027import javax.transaction.Synchronization; 028import javax.transaction.SystemException; 029import javax.transaction.Transaction; 030import javax.transaction.TransactionManager; 031import javax.transaction.TransactionSynchronizationRegistry; 032import javax.transaction.UserTransaction; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.runtime.jtajca.NuxeoContainer; 037 038/** 039 * Utilities to work with transactions. 040 */ 041public class TransactionHelper { 042 043 private static final Log log = LogFactory.getLog(TransactionHelper.class); 044 045 private TransactionHelper() { 046 // utility class 047 } 048 049 /** 050 * Looks up the User Transaction in JNDI. 051 * 052 * @return the User Transaction 053 * @throws NamingException if not found 054 */ 055 public static UserTransaction lookupUserTransaction() throws NamingException { 056 UserTransaction ut = NuxeoContainer.getUserTransaction(); 057 if (ut == null) { 058 throw new NamingException("tx manager not installed"); 059 } 060 return ut; 061 } 062 063 /** 064 * Returns the UserTransaction JNDI binding name. 065 * <p> 066 * Assumes {@link #lookupUserTransaction} has been called once before. 067 */ 068 public static String getUserTransactionJNDIName() { 069 return NuxeoContainer.nameOf("UserTransaction"); 070 } 071 072 /** 073 * Looks up the TransactionManager in JNDI. 074 * 075 * @return the TransactionManager 076 * @throws NamingException if not found 077 */ 078 public static TransactionManager lookupTransactionManager() throws NamingException { 079 TransactionManager tm = NuxeoContainer.getTransactionManager(); 080 if (tm == null) { 081 throw new NamingException("tx manager not installed"); 082 } 083 return tm; 084 } 085 086 /** 087 * Looks up the TransactionSynchronizationRegistry in JNDI. 088 * 089 * @return the TransactionSynchronizationRegistry 090 * @throws NamingException if not found 091 */ 092 public static TransactionSynchronizationRegistry lookupSynchronizationRegistry() throws NamingException { 093 TransactionSynchronizationRegistry synch = NuxeoContainer.getTransactionSynchronizationRegistry(); 094 if (synch == null) { 095 throw new NamingException("tx manager not installed"); 096 } 097 return synch; 098 } 099 100 /** 101 * Checks if there is no transaction 102 * 103 * @6.0 104 */ 105 public static boolean isNoTransaction() { 106 try { 107 return lookupUserTransaction().getStatus() == Status.STATUS_NO_TRANSACTION; 108 } catch (NamingException | SystemException cause) { 109 return true; 110 } 111 } 112 113 /** 114 * Checks if the current User Transaction is active. 115 */ 116 public static boolean isTransactionActive() { 117 try { 118 return lookupUserTransaction().getStatus() == Status.STATUS_ACTIVE; 119 } catch (NamingException | SystemException e) { 120 return false; 121 } 122 } 123 124 /** 125 * Checks if the current User Transaction is marked rollback only. 126 */ 127 public static boolean isTransactionMarkedRollback() { 128 try { 129 return lookupUserTransaction().getStatus() == Status.STATUS_MARKED_ROLLBACK; 130 } catch (NamingException | SystemException e) { 131 return false; 132 } 133 } 134 135 /** 136 * Checks if the current User Transaction is active or marked rollback only. 137 */ 138 public static boolean isTransactionActiveOrMarkedRollback() { 139 try { 140 int status = lookupUserTransaction().getStatus(); 141 return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK; 142 } catch (NamingException | SystemException e) { 143 return false; 144 } 145 } 146 147 /** 148 * Checks if the current User Transaction has already timed out, i.e., whether a commit would immediately abort with 149 * a timeout exception. 150 * 151 * @return {@code true} if there is a current transaction that has timed out, {@code false} otherwise 152 * @since 7.1 153 */ 154 public static boolean isTransactionTimedOut() { 155 TransactionManager tm = NuxeoContainer.getTransactionManager(); 156 if (tm == null) { 157 return false; 158 } 159 try { 160 Transaction tx = tm.getTransaction(); 161 if (tx == null || tx.getStatus() != Status.STATUS_ACTIVE) { 162 return false; 163 } 164 if (tx instanceof org.apache.geronimo.transaction.manager.TransactionImpl) { 165 // Geronimo Transaction Manager 166 Field f = tx.getClass().getDeclaredField("timeout"); 167 f.setAccessible(true); 168 Long timeout = (Long) f.get(tx); 169 return System.currentTimeMillis() > timeout.longValue(); 170 } else { 171 // unknown transaction manager 172 return false; 173 } 174 } catch (SystemException | ReflectiveOperationException e) { 175 throw new RuntimeException(e); 176 } 177 } 178 179 /** 180 * Starts a new User Transaction. 181 * 182 * @return {@code true} if the transaction was successfully started, {@code false} otherwise 183 */ 184 public static boolean startTransaction() { 185 UserTransaction ut = NuxeoContainer.getUserTransaction(); 186 if (ut == null) { 187 return false; 188 } 189 try { 190 if (log.isDebugEnabled()) { 191 log.debug("Starting transaction"); 192 } 193 ut.begin(); 194 return true; 195 } catch (NotSupportedException | SystemException e) { 196 log.error("Unable to start transaction", e); 197 } 198 return false; 199 } 200 201 /** 202 * Suspend the current transaction if active and start a new transaction 203 * 204 * @return the suspended transaction or null 205 * @throws TransactionRuntimeException 206 * @since 5.6 207 */ 208 public static Transaction requireNewTransaction() { 209 TransactionManager tm = NuxeoContainer.getTransactionManager(); 210 if (tm == null) { 211 return null; 212 } 213 try { 214 Transaction tx = tm.getTransaction(); 215 if (tx != null) { 216 tx = tm.suspend(); 217 } 218 tm.begin(); 219 return tx; 220 } catch (NotSupportedException | SystemException e) { 221 throw new TransactionRuntimeException("Cannot suspend tx", e); 222 } 223 } 224 225 public static Transaction suspendTransaction() { 226 TransactionManager tm = NuxeoContainer.getTransactionManager(); 227 if (tm == null) { 228 return null; 229 } 230 try { 231 Transaction tx = tm.getTransaction(); 232 if (tx != null) { 233 tx = tm.suspend(); 234 } 235 return tx; 236 } catch (SystemException e) { 237 throw new TransactionRuntimeException("Cannot suspend tx", e); 238 } 239 } 240 241 /** 242 * Commit the current transaction if active and resume the principal transaction 243 * 244 * @param tx 245 */ 246 public static void resumeTransaction(Transaction tx) { 247 TransactionManager tm = NuxeoContainer.getTransactionManager(); 248 if (tm == null) { 249 return; 250 } 251 try { 252 if (tm.getStatus() == Status.STATUS_ACTIVE) { 253 tm.commit(); 254 } 255 if (tx != null) { 256 tm.resume(tx); 257 } 258 } catch (SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException 259 | InvalidTransactionException | IllegalStateException | SecurityException e) { 260 throw new TransactionRuntimeException("Cannot resume tx", e); 261 } 262 } 263 264 /** 265 * Starts a new User Transaction with the specified timeout. 266 * 267 * @param timeout the timeout in seconds, <= 0 for the default 268 * @return {@code true} if the transaction was successfully started, {@code false} otherwise 269 * @since 5.6 270 */ 271 public static boolean startTransaction(int timeout) { 272 if (timeout < 0) { 273 timeout = 0; 274 } 275 TransactionManager tm = NuxeoContainer.getTransactionManager(); 276 if (tm == null) { 277 return false; 278 } 279 280 try { 281 tm.setTransactionTimeout(timeout); 282 } catch (SystemException e) { 283 log.error("Unable to set transaction timeout: " + timeout, e); 284 return false; 285 } 286 try { 287 return startTransaction(); 288 } finally { 289 try { 290 tm.setTransactionTimeout(0); 291 } catch (SystemException e) { 292 log.error("Unable to reset transaction timeout", e); 293 } 294 } 295 } 296 297 /** 298 * Commits or rolls back the User Transaction depending on the transaction status. 299 */ 300 public static void commitOrRollbackTransaction() { 301 UserTransaction ut = NuxeoContainer.getUserTransaction(); 302 if (ut == null) { 303 return; 304 } 305 try { 306 int status = ut.getStatus(); 307 if (status == Status.STATUS_ACTIVE) { 308 if (log.isDebugEnabled()) { 309 log.debug("Commiting transaction"); 310 } 311 ut.commit(); 312 } else if (status == Status.STATUS_MARKED_ROLLBACK) { 313 if (log.isDebugEnabled()) { 314 log.debug("Cannot commit transaction because it is marked rollback only"); 315 } 316 ut.rollback(); 317 } else { 318 if (log.isDebugEnabled()) { 319 log.debug("Cannot commit transaction with unknown status: " + status); 320 } 321 } 322 } catch (SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException 323 | IllegalStateException | SecurityException e) { 324 String msg = "Unable to commit/rollback"; 325 if (e instanceof RollbackException 326 && "Unable to commit: transaction marked for rollback".equals(e.getMessage())) { 327 // don't log as error, this happens if there's a 328 // ConcurrentModificationException at transaction end inside VCS 329 log.debug(msg, e); 330 } else { 331 log.error(msg, e); 332 } 333 throw new TransactionRuntimeException(msg + ": " + e.getMessage(), e); 334 } 335 } 336 337 private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>(); 338 339 /** 340 * After this, some exceptions during transaction commit may be suppressed and remembered. 341 * 342 * @since 5.9.4 343 */ 344 public static void noteSuppressedExceptions() { 345 suppressedExceptions.set(new ArrayList<Exception>(1)); 346 } 347 348 /** 349 * If activated by {@linked #noteSuppressedExceptions}, remembers the exception. 350 * 351 * @since 5.9.4 352 */ 353 public static void noteSuppressedException(Exception e) { 354 List<Exception> exceptions = suppressedExceptions.get(); 355 if (exceptions != null) { 356 exceptions.add(e); 357 } 358 } 359 360 /** 361 * Gets the suppressed exceptions, and stops remembering. 362 * 363 * @since 5.9.4 364 */ 365 public static List<Exception> getSuppressedExceptions() { 366 List<Exception> exceptions = suppressedExceptions.get(); 367 suppressedExceptions.remove(); 368 return exceptions == null ? Collections.<Exception> emptyList() : exceptions; 369 } 370 371 /** 372 * Sets the current User Transaction as rollback only. 373 * 374 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 375 */ 376 public static boolean setTransactionRollbackOnly() { 377 if (log.isDebugEnabled()) { 378 log.debug("Setting transaction as rollback only"); 379 if (log.isTraceEnabled()) { 380 log.trace("Rollback stack trace", new Throwable("Rollback stack trace")); 381 } 382 } 383 UserTransaction ut = NuxeoContainer.getUserTransaction(); 384 if (ut == null) { 385 return false; 386 } 387 try { 388 ut.setRollbackOnly(); 389 return true; 390 } catch (IllegalStateException | SystemException cause) { 391 log.error("Could not mark transaction as rollback only", cause); 392 } 393 return false; 394 } 395 396 /** 397 * Sets the current User Transaction as rollback only if it has timed out. 398 * 399 * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise 400 * @since 7.1 401 */ 402 public static boolean setTransactionRollbackOnlyIfTimedOut() { 403 if (isTransactionTimedOut()) { 404 return setTransactionRollbackOnly(); 405 } 406 return false; 407 } 408 409 public static void registerSynchronization(Synchronization handler) { 410 if (!isTransactionActiveOrMarkedRollback()) { 411 return; 412 } 413 try { 414 NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler); 415 } catch (IllegalStateException | RollbackException | SystemException cause) { 416 throw new RuntimeException("Cannot register synch handler in current tx", cause); 417 } 418 } 419 420}