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