001/*
002 * (C) Copyright 2011-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 *     matic
018 */
019package org.nuxeo.ecm.core.management.jtajca.internal;
020
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import javax.naming.NamingException;
027import javax.transaction.Status;
028import javax.transaction.Synchronization;
029import javax.transaction.Transaction;
030import javax.transaction.TransactionManager;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.geronimo.transaction.manager.TransactionImpl;
035import org.apache.geronimo.transaction.manager.TransactionManagerImpl;
036import org.apache.geronimo.transaction.manager.TransactionManagerMonitor;
037import org.apache.geronimo.transaction.manager.XidImpl;
038import org.apache.logging.log4j.ThreadContext;
039import org.javasimon.SimonManager;
040import org.javasimon.Stopwatch;
041import org.nuxeo.ecm.core.management.jtajca.TransactionMonitor;
042import org.nuxeo.ecm.core.management.jtajca.TransactionStatistics;
043import org.nuxeo.ecm.core.management.jtajca.internal.DefaultMonitorComponent.ServerInstance;
044import org.nuxeo.runtime.jtajca.NuxeoContainer;
045import org.nuxeo.runtime.transaction.TransactionHelper;
046
047/**
048 * @author matic
049 */
050public class DefaultTransactionMonitor implements TransactionManagerMonitor, TransactionMonitor, Synchronization {
051
052    protected static final Log log = LogFactory.getLog(DefaultTransactionMonitor.class);
053
054    protected TransactionManagerImpl tm;
055
056    protected boolean enabled;
057
058    @Override
059    public void install() {
060        tm = lookup();
061        if (tm == null) {
062            log.warn("Cannot monitor transactions, not a geronimo tx manager");
063            return;
064        }
065        bindManagementInterface();
066        toggle();
067    }
068
069    @Override
070    public void uninstall() {
071        if (tm == null) {
072            return;
073        }
074        unbindManagementInterface();
075        if (enabled) {
076            toggle();
077        }
078    }
079
080    protected ServerInstance self;
081
082    protected void bindManagementInterface() {
083        self = DefaultMonitorComponent.bind(TransactionMonitor.class, this);
084    }
085
086    protected void unbindManagementInterface() {
087        DefaultMonitorComponent.unbind(self);
088        self = null;
089    }
090
091    protected TransactionManagerImpl lookup() {
092        TransactionManager tm = NuxeoContainer.getTransactionManager();
093        if (tm == null) { // try setup trough NuxeoTransactionManagerFactory
094            try {
095                tm = TransactionHelper.lookupTransactionManager();
096            } catch (NamingException cause) {
097                throw new RuntimeException("Cannot lookup tx manager", cause);
098            }
099        }
100        if (!(tm instanceof TransactionManagerImpl)) {
101            return null;
102        }
103        return (TransactionManagerImpl) tm;
104    }
105
106    protected TransactionStatistics lastCommittedStatistics;
107
108    protected TransactionStatistics lastRollbackedStatistics;
109
110    protected final Map<Object, DefaultTransactionStatistics> activeStatistics = new HashMap<>();
111
112    public static String id(Object key) {
113        if (key instanceof XidImpl) {
114            byte[] globalId = ((XidImpl) key).getGlobalTransactionId();
115            StringBuilder sb = new StringBuilder();
116            for (byte aGlobalId : globalId) {
117                sb.append(Integer.toHexString(aGlobalId));
118            }
119            return sb.toString().replaceAll("0*$", "");
120        }
121        return key.toString();
122    }
123
124    public static String id(Transaction tx) {
125        return Integer.toHexString(tx.hashCode());
126    }
127
128    @Override
129    public void threadAssociated(Transaction tx) {
130        long now = System.currentTimeMillis();
131        Object key = tm.getTransactionKey();
132        ThreadContext.put("tx", id(key));
133        Stopwatch sw = SimonManager.getStopwatch("tx");
134        final Thread thread = Thread.currentThread();
135        DefaultTransactionStatistics info = new DefaultTransactionStatistics(key);
136        info.split = sw.start();
137        info.threadName = thread.getName();
138        info.status = TransactionStatistics.Status.fromTx(tx);
139
140        info.startTimestamp = now;
141        info.startCapturedContext = new Throwable("** start invoke context **");
142        synchronized (this) {
143            activeStatistics.put(key, info);
144        }
145        if (TransactionStatistics.Status.ACTIVE == info.status) {
146            tm.registerInterposedSynchronization(this); // register end status
147        }
148        if (log.isTraceEnabled()) {
149            log.trace(info.toString());
150        }
151    }
152
153    @Override
154    public void threadUnassociated(Transaction tx) {
155        try {
156            Object key = ((TransactionImpl) tx).getTransactionKey();
157            DefaultTransactionStatistics stats;
158            synchronized (DefaultTransactionMonitor.class) {
159                stats = activeStatistics.remove(key);
160            }
161            if (stats == null) {
162                log.debug(key + " not found in active statistics map");
163                return;
164            }
165            stats.split.stop();
166            stats.split = null;
167            if (log.isTraceEnabled()) {
168                log.trace(stats);
169            }
170            if (TransactionStatistics.Status.COMMITTED.equals(stats.status)) {
171                lastCommittedStatistics = stats;
172            } else if (TransactionStatistics.Status.ROLLEDBACK.equals(stats.status)) {
173                lastRollbackedStatistics = stats;
174            }
175        } finally {
176            ThreadContext.remove("tx");
177        }
178    }
179
180    @Override
181    public List<TransactionStatistics> getActiveStatistics() {
182        List<TransactionStatistics> l = new ArrayList<>(activeStatistics.values());
183        l.sort((o1, o2) -> o1.getStartDate().compareTo(o2.getEndDate()));
184        return l;
185    }
186
187    @Override
188    public long getActiveCount() {
189        return tm.getActiveCount();
190    }
191
192    @Override
193    public long getTotalCommits() {
194        return tm.getTotalCommits();
195    }
196
197    @Override
198    public long getTotalRollbacks() {
199        return tm.getTotalRollbacks();
200    }
201
202    @Override
203    public TransactionStatistics getLastCommittedStatistics() {
204        return lastCommittedStatistics;
205    }
206
207    @Override
208    public TransactionStatistics getLastRollbackedStatistics() {
209        return lastRollbackedStatistics;
210    }
211
212    protected DefaultTransactionStatistics thisStatistics() {
213        Object key = tm.getTransactionKey();
214        DefaultTransactionStatistics stats;
215        synchronized (this) {
216            stats = activeStatistics.get(key);
217        }
218        if (stats == null) {
219            log.debug(key + " not found in active statistics map");
220        }
221        return stats;
222    }
223
224    @Override
225    public void beforeCompletion() {
226        DefaultTransactionStatistics stats = thisStatistics();
227        if (stats == null) {
228            return;
229        }
230        stats.endCapturedContext = new Throwable("** end invoke context **");
231    }
232
233    @Override
234    public void afterCompletion(int code) {
235        DefaultTransactionStatistics stats = thisStatistics();
236        if (stats == null) {
237            return;
238        }
239        stats.endTimestamp = System.currentTimeMillis();
240        stats.status = TransactionStatistics.Status.fromCode(code);
241        switch (code) {
242        case Status.STATUS_COMMITTED:
243            lastCommittedStatistics = stats;
244            break;
245        case Status.STATUS_ROLLEDBACK:
246            lastRollbackedStatistics = stats;
247            stats.endCapturedContext = new Throwable("** rollback context **");
248            break;
249        }
250    }
251
252    @Override
253    public boolean toggle() {
254        if (enabled) {
255            tm.removeTransactionAssociationListener(this);
256            activeStatistics.clear();
257            enabled = false;
258        } else {
259            tm.addTransactionAssociationListener(this);
260            enabled = true;
261        }
262        return enabled;
263    }
264
265    @Override
266    public boolean getEnabled() {
267        return enabled;
268    }
269
270}