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        try {
354            int status = ut.getStatus();
355            if (status == Status.STATUS_ACTIVE) {
356                if (log.isDebugEnabled()) {
357                    log.debug("Commiting transaction");
358                }
359                ut.commit();
360            } else if (status == Status.STATUS_MARKED_ROLLBACK) {
361                if (log.isDebugEnabled()) {
362                    log.debug("Cannot commit transaction because it is marked rollback only");
363                }
364                ut.rollback();
365            } else {
366                if (log.isDebugEnabled()) {
367                    log.debug("Cannot commit transaction with unknown status: " + status);
368                }
369            }
370        } catch (SystemException | RollbackException | HeuristicMixedException | HeuristicRollbackException
371                | IllegalStateException | SecurityException e) {
372            String msg = "Unable to commit/rollback";
373            String message = e.getMessage();
374            if (e instanceof RollbackException) {
375                switch (message) {
376                case "Unable to commit: transaction marked for rollback":
377                    // don't log as error, this happens if there's a ConcurrentModificationException
378                    // at transaction end inside VCS
379                case "Unable to commit: Transaction timeout":
380                    // don't log either
381                    log.debug(msg, e);
382                    break;
383                default:
384                    log.error(msg, e);
385                }
386            } else {
387                log.error(msg, e);
388            }
389            throw new TransactionRuntimeException(msg + ": " + message, e);
390        }
391    }
392
393    private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>();
394
395    /**
396     * After this, some exceptions during transaction commit may be suppressed and remembered.
397     *
398     * @since 5.9.4
399     */
400    public static void noteSuppressedExceptions() {
401        suppressedExceptions.set(new ArrayList<Exception>(1));
402    }
403
404    /**
405     * If activated by {@linked #noteSuppressedExceptions}, remembers the exception.
406     *
407     * @since 5.9.4
408     */
409    public static void noteSuppressedException(Exception e) {
410        List<Exception> exceptions = suppressedExceptions.get();
411        if (exceptions != null) {
412            exceptions.add(e);
413        }
414    }
415
416    /**
417     * Gets the suppressed exceptions, and stops remembering.
418     *
419     * @since 5.9.4
420     */
421    public static List<Exception> getSuppressedExceptions() {
422        List<Exception> exceptions = suppressedExceptions.get();
423        suppressedExceptions.remove();
424        return exceptions == null ? Collections.<Exception> emptyList() : exceptions;
425    }
426
427    /**
428     * Sets the current User Transaction as rollback only.
429     *
430     * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise
431     */
432    public static boolean setTransactionRollbackOnly() {
433        if (log.isDebugEnabled()) {
434            log.debug("Setting transaction as rollback only");
435            if (log.isTraceEnabled()) {
436                log.trace("Rollback stack trace", new Throwable("Rollback stack trace"));
437            }
438        }
439        UserTransaction ut = NuxeoContainer.getUserTransaction();
440        if (ut == null) {
441            return false;
442        }
443        try {
444            ut.setRollbackOnly();
445            return true;
446        } catch (IllegalStateException | SystemException cause) {
447            log.error("Could not mark transaction as rollback only", cause);
448        }
449        return false;
450    }
451
452    /**
453     * Sets the current User Transaction as rollback only if it has timed out.
454     *
455     * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise
456     * @since 7.1
457     */
458    public static boolean setTransactionRollbackOnlyIfTimedOut() {
459        if (isTransactionTimedOut()) {
460            return setTransactionRollbackOnly();
461        }
462        return false;
463    }
464
465    public static void registerSynchronization(Synchronization handler) {
466        if (!isTransactionActiveOrPreparing()) {
467            throw new TransactionRuntimeException("Cannot register Synchronization if transaction is not active");
468        }
469        try {
470            NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler);
471        } catch (IllegalStateException | RollbackException | SystemException cause) {
472            throw new RuntimeException("Cannot register synch handler in current tx", cause);
473        }
474    }
475
476    /**
477     * Runs the given {@link Runnable} in a transactional context. Will not start a new transaction if one already
478     * exists.
479     *
480     * @param runnable the {@link Runnable}
481     * @since 8.4
482     */
483    public static void runInTransaction(Runnable runnable) {
484        boolean startTransaction = !isTransactionActiveOrMarkedRollback();
485        if (startTransaction) {
486            if (!startTransaction()) {
487                throw new TransactionRuntimeException("Cannot start transaction");
488            }
489        }
490        boolean completedAbruptly = true;
491        try {
492            runnable.run();
493            completedAbruptly = false;
494        } finally {
495            try {
496                if (completedAbruptly) {
497                    setTransactionRollbackOnly();
498                }
499            } finally {
500                if (startTransaction) {
501                    commitOrRollbackTransaction();
502                }
503            }
504        }
505    }
506
507    /**
508     * Calls the given {@link Supplier} in a transactional context. Will not start a new transaction if one already
509     * exists.
510     *
511     * @param supplier the {@link Supplier}
512     * @return the supplier's result
513     * @since 8.4
514     */
515    public static <R> R runInTransaction(Supplier<R> supplier) {
516        boolean startTransaction = !isTransactionActiveOrMarkedRollback();
517        if (startTransaction) {
518            if (!startTransaction()) {
519                throw new TransactionRuntimeException("Cannot start transaction");
520            }
521        }
522        boolean completedAbruptly = true;
523        try {
524            R result = supplier.get();
525            completedAbruptly = false;
526            return result;
527        } finally {
528            try {
529                if (completedAbruptly) {
530                    setTransactionRollbackOnly();
531                }
532            } finally {
533                if (startTransaction) {
534                    commitOrRollbackTransaction();
535                }
536            }
537        }
538    }
539
540}