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}