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}