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