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 <[email protected]> 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.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 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 implements RunnerFeature { 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 /** 058 * @deprecated since 10.3, use {@link #await(Duration)} instead. 059 */ 060 @Deprecated 061 default boolean await(long deadline) throws InterruptedException { 062 return await(Duration.ofMillis(deadline - System.currentTimeMillis())); 063 } 064 065 /** 066 * @since 10.3 067 */ 068 boolean await(Duration duration) throws InterruptedException; 069 070 } 071 072 public void addWaiter(Waiter waiter) { 073 waiters.add(waiter); 074 } 075 076 public void nextTransaction() { 077 nextTransaction(Duration.ofMinutes(3)); 078 } 079 080 /** 081 * @deprecated since 10.3, use {@link #nextTransaction(Duration)} instead. 082 */ 083 @Deprecated 084 public void nextTransaction(long duration, TimeUnit unit) { 085 nextTransaction(Duration.ofMillis(unit.toMillis(duration))); 086 } 087 088 public void nextTransaction(Duration duration) { 089 boolean tx = TransactionHelper.isTransactionActive(); 090 boolean rb = TransactionHelper.isTransactionMarkedRollback(); 091 if (tx || rb) { 092 // there may be tx synchronizer pending, so we 093 // have to commit the transaction 094 TransactionHelper.commitOrRollbackTransaction(); 095 } 096 try { 097 Duration remainingDuration = duration; 098 for (Waiter provider : waiters) { 099 long start = System.currentTimeMillis(); 100 try { 101 await(provider, remainingDuration); 102 } catch (InterruptedException cause) { 103 Thread.currentThread().interrupt(); 104 throw new AssertionError("interrupted while awaiting for asynch completion", cause); 105 } 106 long end = System.currentTimeMillis(); 107 remainingDuration = remainingDuration.minusMillis(end - start); 108 } 109 } finally { 110 if (tx || rb) { 111 // restore previous tx status 112 TransactionHelper.startTransaction(); 113 if (rb) { 114 TransactionHelper.setTransactionRollbackOnly(); 115 } 116 } 117 } 118 } 119 120 protected void await(Waiter waiter, Duration duration) throws InterruptedException { 121 if (!waiter.await(duration)) { 122 try { 123 File file = new ThreadDeadlocksDetector().dump(new long[0]); 124 log.warn("timed out in " + waiter.getClass() + ", thread dump available in " + file); 125 } catch (IOException cause) { 126 log.warn("timed out in " + waiter.getClass() + ", cannot take thread dump", cause); 127 } 128 } 129 } 130 131 @Override 132 public void initialize(FeaturesRunner runner) { 133 autoStartTransaction = runner.getConfig(TransactionalConfig.class).autoStart(); 134 runner.getFeature(RuntimeFeature.class).registerHandler(new TransactionalDeployer()); 135 } 136 137 @Override 138 public void beforeSetup(FeaturesRunner runner) { 139 startTransactionBefore(); 140 } 141 142 @Override 143 public void afterTeardown(FeaturesRunner runner) { 144 commitOrRollbackTransactionAfter(); 145 } 146 147 protected void startTransactionBefore() { 148 if (autoStartTransaction) { 149 txStarted = TransactionHelper.startTransaction(); 150 } 151 } 152 153 protected void commitOrRollbackTransactionAfter() { 154 if (txStarted) { 155 TransactionHelper.commitOrRollbackTransaction(); 156 } else { 157 if (TransactionHelper.isTransactionActive()) { 158 try { 159 TransactionHelper.setTransactionRollbackOnly(); 160 TransactionHelper.commitOrRollbackTransaction(); 161 } finally { 162 log.warn("Committing a transaction for your, please do it yourself"); 163 } 164 } 165 } 166 } 167 168 /** 169 * Handler used to commit transaction before next action and start a new one after next action if 170 * {@link TransactionalConfig#autoStart()} is true. This is because framework is about to be reloaded, then a new 171 * transaction manager will be installed. 172 * 173 * @since 10.2 174 */ 175 public class TransactionalDeployer extends ActionHandler { 176 177 @Override 178 public void exec(String action, String... args) throws Exception { 179 commitOrRollbackTransactionAfter(); 180 next.exec(action, args); 181 startTransactionBefore(); 182 } 183 184 } 185}