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