001/*
002 * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     matic
016 */
017package org.nuxeo.ecm.core.management.jtajca.internal;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import javax.management.ObjectInstance;
027import javax.naming.NamingException;
028import javax.transaction.Status;
029import javax.transaction.Synchronization;
030import javax.transaction.Transaction;
031import javax.transaction.TransactionManager;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.apache.geronimo.transaction.manager.TransactionImpl;
036import org.apache.geronimo.transaction.manager.TransactionManagerImpl;
037import org.apache.geronimo.transaction.manager.TransactionManagerMonitor;
038import org.apache.geronimo.transaction.manager.XidImpl;
039import org.apache.log4j.MDC;
040import org.javasimon.SimonManager;
041import org.javasimon.Stopwatch;
042import org.nuxeo.ecm.core.management.jtajca.TransactionMonitor;
043import org.nuxeo.ecm.core.management.jtajca.TransactionStatistics;
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    }
067
068    @Override
069    public void uninstall() {
070        if (tm == null) {
071            return;
072        }
073        unbindManagementInterface();
074        if (enabled) {
075            toggle();
076        }
077    }
078
079    protected ObjectInstance self;
080
081    protected void bindManagementInterface() {
082        self = DefaultMonitorComponent.bind(TransactionMonitor.class, this);
083    }
084
085    protected void unbindManagementInterface() {
086        DefaultMonitorComponent.unbind(self);
087        self = null;
088    }
089
090    protected TransactionManagerImpl lookup() {
091        TransactionManager tm = NuxeoContainer.getTransactionManager();
092        if (tm == null) { // try setup trough NuxeoTransactionManagerFactory
093            try {
094                tm = TransactionHelper.lookupTransactionManager();
095            } catch (NamingException cause) {
096                throw new RuntimeException("Cannot lookup tx manager", cause);
097            }
098        }
099        if (!(tm instanceof TransactionManagerImpl)) {
100            return null;
101        }
102        return (TransactionManagerImpl) tm;
103    }
104
105    protected TransactionStatistics lastCommittedStatistics;
106
107    protected TransactionStatistics lastRollbackedStatistics;
108
109    protected final Map<Object, DefaultTransactionStatistics> activeStatistics = new HashMap<Object, DefaultTransactionStatistics>();
110
111    public static String id(Object key) {
112        if (key instanceof XidImpl) {
113            byte[] globalId = ((XidImpl) key).getGlobalTransactionId();
114            StringBuffer buffer = new StringBuffer();
115            for (int i = 0; i < globalId.length; i++) {
116                buffer.append(Integer.toHexString(globalId[i]));
117            }
118            return buffer.toString().replaceAll("0*$", "");
119        }
120        return key.toString();
121    }
122
123    public static String id(Transaction tx) {
124        return Integer.toHexString(tx.hashCode());
125    }
126
127    @Override
128    public void threadAssociated(Transaction tx) {
129        long now = System.currentTimeMillis();
130        Object key = tm.getTransactionKey();
131        MDC.put("tx", id(key));
132        Stopwatch sw = SimonManager.getStopwatch("tx");
133        final Thread thread = Thread.currentThread();
134        DefaultTransactionStatistics info = new DefaultTransactionStatistics(key);
135        info.split = sw.start();
136        info.threadName = thread.getName();
137        info.status = TransactionStatistics.Status.fromTx(tx);
138
139        info.startTimestamp = now;
140        info.startCapturedContext = new Throwable("** start invoke context **");
141        synchronized (this) {
142            activeStatistics.put(key, info);
143        }
144        tm.registerInterposedSynchronization(this); // register end status
145        if (log.isTraceEnabled()) {
146            log.trace(info.toString());
147        }
148    }
149
150    @Override
151    public void threadUnassociated(Transaction tx) {
152        try {
153            Object key = ((TransactionImpl) tx).getTransactionKey();
154            DefaultTransactionStatistics stats;
155            synchronized (DefaultTransactionMonitor.class) {
156                stats = activeStatistics.remove(key);
157            }
158            if (stats == null) {
159                log.debug(key + " not found in active statistics map");
160                return;
161            }
162            stats.split.stop();
163            stats.split = null;
164            if (log.isTraceEnabled()) {
165                log.trace(stats);
166            }
167            if (TransactionStatistics.Status.COMMITTED.equals(stats.status)) {
168                lastCommittedStatistics = stats;
169            } else if (TransactionStatistics.Status.ROLLEDBACK.equals(stats.status)) {
170                lastRollbackedStatistics = stats;
171            }
172        } finally {
173            MDC.remove("tx");
174        }
175    }
176
177    @Override
178    public List<TransactionStatistics> getActiveStatistics() {
179        List<TransactionStatistics> l = new ArrayList<TransactionStatistics>(activeStatistics.values());
180        Collections.sort(l, new Comparator<TransactionStatistics>() {
181            @Override
182            public int compare(TransactionStatistics o1, TransactionStatistics o2) {
183                return o1.getStartDate().compareTo(o2.getEndDate());
184            }
185        });
186        return l;
187    }
188
189    @Override
190    public long getActiveCount() {
191        return tm.getActiveCount();
192    }
193
194    @Override
195    public long getTotalCommits() {
196        return tm.getTotalCommits();
197    }
198
199    @Override
200    public long getTotalRollbacks() {
201        return tm.getTotalRollbacks();
202    }
203
204    @Override
205    public TransactionStatistics getLastCommittedStatistics() {
206        return lastCommittedStatistics;
207    }
208
209    @Override
210    public TransactionStatistics getLastRollbackedStatistics() {
211        return lastRollbackedStatistics;
212    }
213
214    protected DefaultTransactionStatistics thisStatistics() {
215        Object key = tm.getTransactionKey();
216        DefaultTransactionStatistics stats;
217        synchronized (this) {
218            stats = activeStatistics.get(key);
219        }
220        if (stats == null) {
221            log.debug(key + " not found in active statistics map");
222        }
223        return stats;
224    }
225
226    @Override
227    public void beforeCompletion() {
228        DefaultTransactionStatistics stats = thisStatistics();
229        if (stats == null) {
230            return;
231        }
232        stats.endCapturedContext = new Throwable("** end invoke context **");
233    }
234
235    @Override
236    public void afterCompletion(int code) {
237        DefaultTransactionStatistics stats = thisStatistics();
238        if (stats == null) {
239            return;
240        }
241        stats.endTimestamp = System.currentTimeMillis();
242        stats.status = TransactionStatistics.Status.fromCode(code);
243        switch (code) {
244        case Status.STATUS_COMMITTED:
245            lastCommittedStatistics = stats;
246            break;
247        case Status.STATUS_ROLLEDBACK:
248            lastRollbackedStatistics = stats;
249            stats.endCapturedContext = new Throwable("** rollback context **");
250            break;
251        }
252    }
253
254    @Override
255    public boolean toggle() {
256        if (enabled) {
257            tm.removeTransactionAssociationListener(this);
258            activeStatistics.clear();
259            enabled = false;
260        } else {
261            tm.addTransactionAssociationListener(this);
262            enabled = true;
263        }
264        return enabled;
265    }
266
267    @Override
268    public boolean getEnabled() {
269        return enabled;
270    }
271
272}