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        noteSuppressedExceptions();
354        RuntimeException thrown = null;
355        boolean isRollbackDuringCommit = false;
356        try {
357            int status = ut.getStatus();
358            if (status == Status.STATUS_ACTIVE) {
359                if (log.isDebugEnabled()) {
360                    log.debug("Committing transaction");
361                }
362                try {
363                    ut.commit();
364                } catch (RollbackException | HeuristicRollbackException | HeuristicMixedException e) {
365                    String msg = "Unable to commit";
366                    // messages from org.apache.geronimo.transaction.manager.TransactionImpl.commit
367                    switch (e.getMessage()) {
368                    case "Unable to commit: transaction marked for rollback":
369                        // don't log as error, this happens if there's a ConcurrentUpdateException
370                        // at transaction end inside VCS
371                        isRollbackDuringCommit = true;
372                        // $FALL-THROUGH$
373                    case "Unable to commit: Transaction timeout":
374                        // don't log either
375                        log.debug(msg, e);
376                        break;
377                    default:
378                        log.error(msg, e);
379                    }
380                    throw new TransactionRuntimeException(e.getMessage(), e);
381                }
382            } else if (status == Status.STATUS_MARKED_ROLLBACK) {
383                if (log.isDebugEnabled()) {
384                    log.debug("Cannot commit transaction because it is marked rollback only");
385                }
386                ut.rollback();
387            } else {
388                if (log.isDebugEnabled()) {
389                    log.debug("Cannot commit transaction with unknown status: " + status);
390                }
391            }
392        } catch (SystemException e) {
393            thrown = new TransactionRuntimeException(e);
394            throw thrown;
395        } catch (RuntimeException e) {
396            thrown = e;
397            throw thrown;
398        } finally {
399            List<Exception> suppressed = getSuppressedExceptions();
400            if (!suppressed.isEmpty()) {
401                // add suppressed to thrown exception, or throw a new one
402                RuntimeException e;
403                if (thrown == null) {
404                    e = new TransactionRuntimeException("Exception during commit");
405                } else {
406                    if (isRollbackDuringCommit && suppressed.get(0) instanceof RuntimeException) {
407                        // use the suppressed one directly and throw it instead
408                        thrown = null; // force rethrow below
409                        e = (RuntimeException) suppressed.remove(0);
410                    } else {
411                        e = thrown;
412                    }
413                }
414                suppressed.forEach(s -> e.addSuppressed(s));
415                if (thrown == null) {
416                    throw e;
417                }
418            }
419        }
420    }
421
422    private static ThreadLocal<List<Exception>> suppressedExceptions = new ThreadLocal<List<Exception>>();
423
424    /**
425     * After this, some exceptions during transaction commit may be suppressed and remembered.
426     *
427     * @since 5.9.4
428     */
429    protected static void noteSuppressedExceptions() {
430        suppressedExceptions.set(new ArrayList<>());
431    }
432
433    /**
434     * Remembers the exception if it happens during the processing of a commit, so that it can be surfaced as a
435     * suppressed exception at the end of the commit.
436     *
437     * @since 5.9.4
438     */
439    public static void noteSuppressedException(Exception e) {
440        List<Exception> exceptions = suppressedExceptions.get();
441        if (exceptions != null) {
442            exceptions.add(e);
443        }
444    }
445
446    /**
447     * Gets the suppressed exceptions, and stops remembering.
448     *
449     * @since 5.9.4
450     */
451    protected static List<Exception> getSuppressedExceptions() {
452        List<Exception> exceptions = suppressedExceptions.get();
453        suppressedExceptions.remove();
454        return exceptions == null ? Collections.<Exception> emptyList() : exceptions;
455    }
456
457    /**
458     * Sets the current User Transaction as rollback only.
459     *
460     * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise
461     */
462    public static boolean setTransactionRollbackOnly() {
463        if (log.isDebugEnabled()) {
464            log.debug("Setting transaction as rollback only");
465            if (log.isTraceEnabled()) {
466                log.trace("Rollback stack trace", new Throwable("Rollback stack trace"));
467            }
468        }
469        UserTransaction ut = NuxeoContainer.getUserTransaction();
470        if (ut == null) {
471            return false;
472        }
473        try {
474            ut.setRollbackOnly();
475            return true;
476        } catch (IllegalStateException | SystemException cause) {
477            log.error("Could not mark transaction as rollback only", cause);
478        }
479        return false;
480    }
481
482    /**
483     * Sets the current User Transaction as rollback only if it has timed out.
484     *
485     * @return {@code true} if the transaction was successfully marked rollback only, {@code false} otherwise
486     * @since 7.1
487     */
488    public static boolean setTransactionRollbackOnlyIfTimedOut() {
489        if (isTransactionTimedOut()) {
490            return setTransactionRollbackOnly();
491        }
492        return false;
493    }
494
495    public static void registerSynchronization(Synchronization handler) {
496        if (!isTransactionActiveOrPreparing()) {
497            throw new TransactionRuntimeException("Cannot register Synchronization if transaction is not active");
498        }
499        try {
500            NuxeoContainer.getTransactionManager().getTransaction().registerSynchronization(handler);
501        } catch (IllegalStateException | RollbackException | SystemException cause) {
502            throw new RuntimeException("Cannot register synch handler in current tx", cause);
503        }
504    }
505
506    /**
507     * Runs the given {@link Runnable} without a  transactional context. Will suspend and restore the transaction if one already
508     * exists.
509     *
510     * @param runnable the {@link Runnable}
511     * @since 9.1
512     */
513    public static void runWithoutTransaction(Runnable runnable) {
514        runWithoutTransaction(() -> { runnable.run(); return null; });
515    }
516
517
518    /**
519     * Calls the given {@link Supplier} without a transactional context. Will suspend and restore the transaction if one already
520     * exists.
521     *
522     * @param supplier the {@link Supplier}
523     * @since 9.1
524     */
525    public static <R> R runWithoutTransaction(Supplier<R> supplier) {
526        Transaction tx = suspendTransaction();
527        try {
528            return supplier.get();
529        } finally {
530            resumeTransaction(tx);
531        }
532    }
533
534    /**
535     * Runs the given {@link Runnable} in a new transactional context. Will suspend and restore the transaction if one already
536     * exists.
537     *
538     * @param runnable the {@link Runnable}
539     * @since 9.1
540     */
541    public static void runInNewTransaction(Runnable runnable) {
542        runInNewTransaction(() -> { runnable.run(); return null;});
543    }
544
545    /**
546     * Calls the given {@link Supplier} in a new transactional context. Will suspend and restore the transaction if one already
547     * exists.
548     *
549     * @param supplier the {@link Supplier}
550     * @since 9.1
551     */
552    public static <R> R runInNewTransaction(Supplier<R> supplier) {
553        Transaction tx = suspendTransaction();
554        try {
555            return runInTransaction(supplier);
556        } finally {
557            resumeTransaction(tx);
558        }
559    }
560
561    /**
562     * Runs the given {@link Runnable} in a transactional context. Will not start a new transaction if one already
563     * exists.
564     *
565     * @param runnable the {@link Runnable}
566     * @since 8.4
567     */
568    public static void runInTransaction(Runnable runnable) {
569        runInTransaction(() -> {runnable.run(); return null;});
570    }
571
572    /**
573     * Calls the given {@link Supplier} in a transactional context. Will not start a new transaction if one already
574     * exists.
575     *
576     * @param supplier the {@link Supplier}
577     * @return the supplier's result
578     * @since 8.4
579     */
580    public static <R> R runInTransaction(Supplier<R> supplier) {
581        boolean startTransaction = !isTransactionActiveOrMarkedRollback();
582        if (startTransaction) {
583            if (!startTransaction()) {
584                throw new TransactionRuntimeException("Cannot start transaction");
585            }
586        }
587        boolean completedAbruptly = true;
588        try {
589            R result = supplier.get();
590            completedAbruptly = false;
591            return result;
592        } finally {
593            try {
594                if (completedAbruptly) {
595                    setTransactionRollbackOnly();
596                }
597            } finally {
598                if (startTransaction) {
599                    commitOrRollbackTransaction();
600                }
601            }
602        }
603    }
604
605}