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