001/*
002 * (C) Copyright 2018 Nuxeo (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 *       Kevin Leturc <kleturc@nuxeo.com>
018 */
019package org.nuxeo.runtime.test.runner;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.concurrent.TimeUnit;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.junit.Assert;
030import org.nuxeo.runtime.management.jvm.ThreadDeadlocksDetector;
031import org.nuxeo.runtime.test.runner.HotDeployer.ActionHandler;
032import org.nuxeo.runtime.transaction.TransactionHelper;
033
034/**
035 * The transactional feature is responsible of transaction management.
036 * <p/>
037 * It brings some API to wait for transaction
038 *
039 * @since 10.2
040 */
041@Deploy("org.nuxeo.runtime.jtajca")
042@Deploy("org.nuxeo.runtime.datasource")
043@Features(RuntimeFeature.class)
044public class TransactionalFeature extends SimpleFeature {
045
046    private static final Log log = LogFactory.getLog(TransactionalFeature.class);
047
048    protected boolean autoStartTransaction;
049
050    protected boolean txStarted;
051
052    protected final List<Waiter> waiters = new LinkedList<>();
053
054    @FunctionalInterface
055    public interface Waiter {
056
057        boolean await(long deadline) throws InterruptedException;
058
059    }
060
061    public void addWaiter(Waiter waiter) {
062        waiters.add(waiter);
063    }
064
065    public void nextTransaction() {
066        nextTransaction(10, TimeUnit.MINUTES);
067    }
068
069    public void nextTransaction(long duration, TimeUnit unit) {
070        long deadline = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(duration, unit);
071        boolean tx = TransactionHelper.isTransactionActive();
072        boolean rb = TransactionHelper.isTransactionMarkedRollback();
073        if (tx || rb) {
074            // there may be tx synchronizer pending, so we
075            // have to commit the transaction
076            TransactionHelper.commitOrRollbackTransaction();
077        }
078        try {
079            for (Waiter provider : waiters) {
080                try {
081                    Assert.assertTrue(await(provider, deadline));
082                } catch (InterruptedException cause) {
083                    Thread.currentThread().interrupt();
084                    throw new AssertionError("interrupted while awaiting for asynch completion", cause);
085                }
086            }
087        } finally {
088            if (tx || rb) {
089                // restore previous tx status
090                TransactionHelper.startTransaction();
091                if (rb) {
092                    TransactionHelper.setTransactionRollbackOnly();
093                }
094            }
095        }
096    }
097
098    boolean await(Waiter waiter, long deadline) throws InterruptedException {
099        if (waiter.await(deadline)) {
100            return true;
101        }
102        try {
103            File file = new ThreadDeadlocksDetector().dump(new long[0]);
104            log.warn("timed out in " + waiter.getClass() + ", thread dump available in " + file);
105        } catch (IOException cause) {
106            log.warn("timed out in " + waiter.getClass() + ", cannot take thread dump", cause);
107        }
108        return false;
109    }
110
111    @Override
112    public void initialize(FeaturesRunner runner) {
113        autoStartTransaction = runner.getConfig(TransactionalConfig.class).autoStart();
114        runner.getFeature(RuntimeFeature.class).registerHandler(new TransactionalDeployer());
115    }
116
117    @Override
118    public void beforeSetup(FeaturesRunner runner) {
119        startTransactionBefore();
120    }
121
122    @Override
123    public void afterTeardown(FeaturesRunner runner) {
124        commitOrRollbackTransactionAfter();
125    }
126
127    protected void startTransactionBefore() {
128        if (autoStartTransaction) {
129            txStarted = TransactionHelper.startTransaction();
130        }
131    }
132
133    protected void commitOrRollbackTransactionAfter() {
134        if (txStarted) {
135            TransactionHelper.commitOrRollbackTransaction();
136        } else {
137            if (TransactionHelper.isTransactionActive()) {
138                try {
139                    TransactionHelper.setTransactionRollbackOnly();
140                    TransactionHelper.commitOrRollbackTransaction();
141                } finally {
142                    log.warn("Committing a transaction for your, please do it yourself");
143                }
144            }
145        }
146    }
147
148    /**
149     * Handler used to commit transaction before next action and start a new one after next action if
150     * {@link TransactionalConfig#autoStart()} is true. This is because framework is about to be reloaded, then a new
151     * transaction manager will be installed.
152     *
153     * @since 10.2
154     */
155    public class TransactionalDeployer extends ActionHandler {
156
157        @Override
158        public void exec(String action, String... args) throws Exception {
159            commitOrRollbackTransactionAfter();
160            next.exec(action, args);
161            startTransactionBefore();
162        }
163
164    }
165}