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