All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glowroot.transaction.model.Transaction Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.glowroot.transaction.model;

import java.lang.management.ThreadInfo;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

import org.glowroot.shaded.google.common.base.Strings;
import org.glowroot.shaded.google.common.base.Ticker;
import org.glowroot.shaded.google.common.collect.HashMultimap;
import org.glowroot.shaded.google.common.collect.ImmutableList;
import org.glowroot.shaded.google.common.collect.ImmutableSetMultimap;
import org.glowroot.shaded.google.common.collect.SetMultimap;
import org.glowroot.shaded.google.common.collect.TreeMultimap;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.glowroot.shaded.slf4j.Logger;
import org.glowroot.shaded.slf4j.LoggerFactory;

import org.glowroot.common.ScheduledRunnable;
import org.glowroot.config.AdvancedConfigBase;
import org.glowroot.jvm.ThreadAllocatedBytes;
import org.glowroot.plugin.api.transaction.MessageSupplier;
import org.glowroot.plugin.api.transaction.TimerName;
import org.glowroot.plugin.api.transaction.internal.ReadableMessage;
import org.glowroot.transaction.ErrorMessage;

import static org.glowroot.shaded.google.common.base.Preconditions.checkNotNull;

// contains all data that has been captured for a given transaction (e.g. a servlet request)
//
// this class needs to be thread safe, only one thread updates it, but multiple threads can read it
// at the same time as it is being updated
public class Transaction {

    private static final Logger logger = LoggerFactory.getLogger(Transaction.class);

    public static final int USE_GENERAL_STORE_THRESHOLD = -1;

    // initial capacity is very important, see ThreadSafeCollectionOfTenBenchmark
    private static final int CUSTOM_ATTRIBUTE_KEYS_INITIAL_CAPACITY = 16;

    // this is just to limit memory (and also to limit display size of trace)
    private static final long CUSTOM_ATTRIBUTE_VALUES_PER_KEY_LIMIT = 10000;

    // a unique identifier
    private final TraceUniqueId id;

    // timing data is tracked in nano seconds which cannot be converted into dates
    // (see javadoc for System.nanoTime()), so the start time is also tracked here
    private final long startTime;

    private volatile String transactionType;
    private volatile @Nullable OverrideSource transactionTypeOverrideSource;

    private volatile String transactionName;
    private volatile @Nullable OverrideSource transactionNameOverrideSource;

    private volatile @Nullable String user;
    private volatile @Nullable OverrideSource userOverrideSource;

    // lazy loaded to reduce memory when custom attributes are not used
    @GuardedBy("customAttributes")
    private volatile @MonotonicNonNull SetMultimap customAttributes;

    // trace-level error
    private volatile @Nullable ErrorMessage errorMessage;
    private volatile @Nullable OverrideSource errorMessageOverrideSource;

    private final TimerImpl rootTimer;
    // currentTimer doesn't need to be thread safe as it is only accessed by transaction thread
    private @Nullable TimerImpl currentTimer;

    private final @Nullable ThreadInfoComponent threadInfoComponent;
    private final @Nullable GcInfoComponent gcInfoComponent;

    // root entry for this trace
    private final TraceEntryComponent traceEntryComponent;

    // linked list of QueryData instances for safe concurrent access
    private @MonotonicNonNull QueryData headQueryData;
    // these maps are only accessed by the transaction thread
    private @MonotonicNonNull Map firstQueryTypeQueries;
    private @MonotonicNonNull Map> allQueryTypesMap;
    private final int maxAggregateQueriesPerQueryType;

    // stack trace data constructed from profiling
    private volatile @MonotonicNonNull Profile profile;

    private final long threadId;

    // overrides general store threshold
    // -1 means don't override the general store threshold
    private volatile int slowThresholdMillis = USE_GENERAL_STORE_THRESHOLD;
    private volatile @Nullable OverrideSource slowThresholdMillisOverrideSource;

    // these are stored in the trace so they are only scheduled a single time, and also so they can
    // be canceled at trace completion
    private volatile @MonotonicNonNull ScheduledRunnable userProfileRunnable;
    private volatile @MonotonicNonNull ScheduledRunnable immedateTraceStoreRunnable;

    private volatile boolean partiallyStored;

    private long captureTime;

    // memory barrier is used to ensure memory visibility of entries and timers at key points,
    // namely after each entry
    //
    // benchmarking shows this is significantly faster than ensuring memory visibility of each
    // timer update, the down side is that the latest updates to timers for transactions
    // that are captured in-flight (e.g. partial traces and active traces displayed in the UI) may
    // not be visible
    private volatile boolean memoryBarrier;

    private final CompletionCallback completionCallback;

    public Transaction(long startTime, String transactionType, String transactionName,
            MessageSupplier messageSupplier, TimerName timerName, long startTick,
            boolean captureThreadInfo, boolean captureGcInfo, int maxAggregateQueriesPerQueryType,
            @Nullable ThreadAllocatedBytes threadAllocatedBytes,
            CompletionCallback completionCallback, Ticker ticker) {
        this.startTime = startTime;
        this.transactionType = transactionType;
        this.transactionName = transactionName;
        id = new TraceUniqueId(startTime);
        // suppress warning for passing @UnderInitialization this
        @SuppressWarnings("argument.type.incompatible")
        TimerImpl rootTimer = TimerImpl.createRootTimer(this, (TimerNameImpl) timerName);
        this.rootTimer = rootTimer;
        rootTimer.start(startTick);
        traceEntryComponent =
                new TraceEntryComponent(messageSupplier, rootTimer, startTick, ticker);
        threadId = Thread.currentThread().getId();
        threadInfoComponent =
                captureThreadInfo ? new ThreadInfoComponent(threadAllocatedBytes) : null;
        gcInfoComponent = captureGcInfo ? new GcInfoComponent() : null;
        this.maxAggregateQueriesPerQueryType = maxAggregateQueriesPerQueryType;
        this.completionCallback = completionCallback;
    }

    public long getStartTime() {
        return startTime;
    }

    public String getId() {
        return id.get();
    }

    // a couple of properties make sense to expose as part of trace
    public long getStartTick() {
        return traceEntryComponent.getStartTick();
    }

    public boolean isCompleted() {
        return traceEntryComponent.isCompleted();
    }

    public long getEndTick() {
        return traceEntryComponent.getEndTick();
    }

    // duration of trace in nanoseconds
    public long getDuration() {
        return traceEntryComponent.getDuration();
    }

    public String getTransactionType() {
        return transactionType;
    }

    public String getTransactionName() {
        return transactionName;
    }

    public String getHeadline() {
        MessageSupplier messageSupplier = traceEntryComponent.getRootEntry().getMessageSupplier();
        // root trace entry messageSupplier is never be null
        checkNotNull(messageSupplier);
        return ((ReadableMessage) messageSupplier.get()).getText();
    }

    public @Nullable String getUser() {
        return user;
    }

    public ImmutableSetMultimap getCustomAttributes() {
        if (customAttributes == null) {
            return ImmutableSetMultimap.of();
        }
        SetMultimap orderedCustomAttributes =
                TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, String.CASE_INSENSITIVE_ORDER);
        synchronized (customAttributes) {
            orderedCustomAttributes.putAll(customAttributes);
        }
        return ImmutableSetMultimap.copyOf(orderedCustomAttributes);
    }

    public Map getCustomDetail() {
        MessageSupplier messageSupplier = traceEntryComponent.getRootEntry().getMessageSupplier();
        // root trace entry messageSupplier is never be null
        checkNotNull(messageSupplier);
        return ((ReadableMessage) messageSupplier.get()).getDetail();
    }

    public @Nullable ErrorMessage getErrorMessage() {
        // don't prefer the root entry error message since it is likely a more generic error
        // message, e.g. servlet response sendError(500)
        if (errorMessage != null) {
            return errorMessage;
        }
        return traceEntryComponent.getRootEntry().getErrorMessage();
    }

    // this is called from a non-transaction thread
    public TimerImpl getRootTimer() {
        readMemoryBarrier();
        return rootTimer;
    }

    public @Nullable TimerImpl getCurrentTimer() {
        return currentTimer;
    }

    // can be called from a non-transaction thread
    public @Nullable ThreadInfoData getThreadInfo() {
        return threadInfoComponent == null ? null : threadInfoComponent.getThreadInfo();
    }

    // can be called from a non-transaction thread
    public @Nullable List getGcInfos() {
        return gcInfoComponent == null ? null : gcInfoComponent.getGcInfos();
    }

    public TraceEntryImpl getRootEntry() {
        return traceEntryComponent.getRootEntry();
    }

    public int getEntryCount() {
        return traceEntryComponent.getEntryCount();
    }

    public Iterable getQueries() {
        // read memory barrier is for QueryData values
        readMemoryBarrier();
        if (headQueryData == null) {
            return ImmutableList.of();
        }
        return new Iterable() {
            @Override
            public Iterator iterator() {
                return new Iterator() {
                    private @Nullable QueryData next = headQueryData;
                    @Override
                    public boolean hasNext() {
                        return next != null;
                    }
                    @Override
                    public QueryData next() {
                        QueryData curr = next;
                        if (curr == null) {
                            throw new NoSuchElementException();
                        }
                        next = curr.getNextQueryData();
                        return curr;
                    }
                    @Override
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }

    public Iterable getEntries() {
        readMemoryBarrier();
        return traceEntryComponent;
    }

    public long getProfileSampleCount() {
        if (profile == null) {
            return 0;
        } else {
            return profile.getSampleCount();
        }
    }

    public @Nullable Profile getProfile() {
        return profile;
    }

    public int getSlowThresholdMillisOverride() {
        return slowThresholdMillis;
    }

    public @Nullable ScheduledRunnable getUserProfileRunnable() {
        return userProfileRunnable;
    }

    public @Nullable ScheduledRunnable getImmedateTraceStoreRunnable() {
        return immedateTraceStoreRunnable;
    }

    public boolean isPartiallyStored() {
        return partiallyStored;
    }

    public long getThreadId() {
        return threadId;
    }

    public void setTransactionType(String transactionType, OverrideSource overrideSource) {
        if (transactionTypeOverrideSource == null
                || transactionTypeOverrideSource.priority < overrideSource.priority) {
            this.transactionType = transactionType;
            transactionTypeOverrideSource = overrideSource;
        }
    }

    public void setTransactionName(String transactionName, OverrideSource overrideSource) {
        if (transactionNameOverrideSource == null
                || transactionNameOverrideSource.priority < overrideSource.priority) {
            this.transactionName = transactionName;
            transactionNameOverrideSource = overrideSource;
        }
    }

    public void setUser(String user, OverrideSource overrideSource) {
        if (userOverrideSource == null || userOverrideSource.priority < overrideSource.priority) {
            this.user = user;
            userOverrideSource = overrideSource;
        }
    }

    public void addCustomAttribute(String name, @Nullable String value) {
        if (customAttributes == null) {
            // no race condition here since only transaction thread calls addAttribute()
            customAttributes = HashMultimap.create(CUSTOM_ATTRIBUTE_KEYS_INITIAL_CAPACITY, 1);
        }
        String val = Strings.nullToEmpty(value);
        synchronized (customAttributes) {
            Collection values = customAttributes.get(name);
            if (values.size() < CUSTOM_ATTRIBUTE_VALUES_PER_KEY_LIMIT) {
                values.add(val);
            }
        }
    }

    public void setError(ErrorMessage errorMessage, OverrideSource overrideSource) {
        if (errorMessageOverrideSource == null
                || errorMessageOverrideSource.priority < overrideSource.priority) {
            this.errorMessage = errorMessage;
            errorMessageOverrideSource = overrideSource;
        }
    }

    public void setSlowThresholdMillis(int slowThresholdMillis, OverrideSource overrideSource) {
        if (slowThresholdMillisOverrideSource == null
                || slowThresholdMillisOverrideSource.priority < overrideSource.priority) {
            this.slowThresholdMillis = slowThresholdMillis;
            slowThresholdMillisOverrideSource = overrideSource;
        } else if (slowThresholdMillisOverrideSource.priority == overrideSource.priority) {
            // use the minimum threshold from the same override source
            this.slowThresholdMillis = Math.min(this.slowThresholdMillis, slowThresholdMillis);
        }
    }

    public void setUserProfileRunnable(ScheduledRunnable scheduledRunnable) {
        if (userProfileRunnable != null) {
            logger.warn("setUserProfileRunnable(): overwriting non-null userProfileRunnable");
        }
        this.userProfileRunnable = scheduledRunnable;
    }

    public void setImmediateTraceStoreRunnable(ScheduledRunnable scheduledRunnable) {
        if (immedateTraceStoreRunnable != null) {
            logger.warn("setImmediateTraceStoreRunnable(): overwriting non-null"
                    + " immedateTraceStoreRunnable");
        }
        this.immedateTraceStoreRunnable = scheduledRunnable;
    }

    public void setPartiallyStored() {
        partiallyStored = true;
    }

    public TraceEntryImpl pushEntry(long startTick, MessageSupplier messageSupplier,
            @Nullable String queryType, @Nullable String queryText, long queryExecutionCount,
            TimerImpl timer) {
        QueryData queryData = null;
        if (queryType != null && queryText != null) {
            queryData = getOrCreateQueryDataIfPossible(queryType, queryText);
        }
        return traceEntryComponent.pushEntry(startTick, messageSupplier, queryData,
                queryExecutionCount, timer);
    }

    // only called by transaction thread
    public @Nullable QueryData getOrCreateQueryDataIfPossible(String queryType, String queryText) {
        if (headQueryData == null) {
            QueryData queryData = new QueryData(queryType, queryText, null);
            // TODO build a micro-optimized query data map, e.g. NestedTimerMap
            firstQueryTypeQueries = new HashMap(4);
            firstQueryTypeQueries.put(queryText, queryData);
            headQueryData = queryData;
            return headQueryData;
        }
        Map currentQueryTypeQueries;
        if (queryType.equals(headQueryData.getQueryType())) {
            currentQueryTypeQueries = checkNotNull(firstQueryTypeQueries);
        } else {
            currentQueryTypeQueries = getOrCreateQueriesForQueryType(queryType);
        }
        QueryData queryData = currentQueryTypeQueries.get(queryText);
        if (queryData == null && currentQueryTypeQueries.size() < maxAggregateQueriesPerQueryType
                * AdvancedConfigBase.OVERALL_AGGREGATE_QUERIES_HARD_LIMIT_MULTIPLIER) {
            queryData = new QueryData(queryType, queryText, headQueryData);
            currentQueryTypeQueries.put(queryText, queryData);
            headQueryData = queryData;
        }
        return queryData;
    }

    public TraceEntryImpl addEntry(long startTick, long endTick,
            @Nullable MessageSupplier messageSupplier, @Nullable ErrorMessage errorMessage,
            boolean limitBypassed) {
        TraceEntryImpl entry = traceEntryComponent.addEntry(startTick, endTick, messageSupplier,
                errorMessage, limitBypassed);
        memoryBarrier = true;
        return entry;
    }

    public void addEntryLimitExceededMarkerIfNeeded() {
        traceEntryComponent.addEntryLimitExceededMarkerIfNeeded();
        memoryBarrier = true;
    }

    public void captureStackTrace(@Nullable ThreadInfo threadInfo, int limit,
            boolean mayHaveSyntheticTimerMethods) {
        if (threadInfo == null) {
            // thread is no longer alive
            return;
        }
        if (traceEntryComponent.isCompleted()) {
            return;
        }
        if (profile == null) {
            // initialization possible race condition (between StackTraceCollector and
            // UserProfileRunnable) is ok, worst case scenario it misses an almost simultaneously
            // captured stack trace
            //
            // profile is constructed and first stack trace is added prior to setting the
            // transaction profile field, so that it is not possible to read a profile that doesn't
            // have at least one stack trace
            Profile profile = new Profile(mayHaveSyntheticTimerMethods);
            profile.addStackTrace(threadInfo, limit);
            this.profile = profile;
        } else {
            profile.addStackTrace(threadInfo, limit);
        }
    }

    // called by the transaction thread
    public void onCompleteCaptureThreadInfo() {
        if (threadInfoComponent != null) {
            threadInfoComponent.onComplete();
        }
    }

    // called by the transaction thread
    public void onCompleteCaptureGcInfo() {
        if (gcInfoComponent != null) {
            gcInfoComponent.onComplete();
        }
    }

    // called by the transaction thread
    public void onComplete(long captureTime) {
        this.captureTime = captureTime;
    }

    public long getCaptureTime() {
        return captureTime;
    }

    // typically pop() methods don't require the objects to pop, but for safety, the entry to pop is
    // passed in just to make sure it is the one on top (and if not, then pop until is is found,
    // preventing any nasty bugs from a missed pop, e.g. a trace never being marked as complete)
    void popEntry(TraceEntryImpl entry, long endTick) {
        traceEntryComponent.popEntry(entry, endTick);
        memoryBarrier = true;
        if (isCompleted()) {
            // the root entry has been popped off
            if (immedateTraceStoreRunnable != null) {
                immedateTraceStoreRunnable.cancel();
            }
            if (userProfileRunnable != null) {
                userProfileRunnable.cancel();
            }
            completionCallback.completed(this);
        }
    }

    void setCurrentTimer(@Nullable TimerImpl currentTimer) {
        this.currentTimer = currentTimer;
    }

    private Map getOrCreateQueriesForQueryType(String queryType) {
        if (allQueryTypesMap == null) {
            allQueryTypesMap = new HashMap>(2);
            Map currentQueryTypeQueries = new HashMap(4);
            allQueryTypesMap.put(queryType, currentQueryTypeQueries);
            return currentQueryTypeQueries;
        }
        Map currentQueryTypeQueries = allQueryTypesMap.get(queryType);
        if (currentQueryTypeQueries == null) {
            currentQueryTypeQueries = new HashMap(4);
            allQueryTypesMap.put(queryType, currentQueryTypeQueries);
        }
        return currentQueryTypeQueries;
    }

    private boolean readMemoryBarrier() {
        return memoryBarrier;
    }

    public static interface CompletionCallback {
        void completed(Transaction transaction);
    }

    public static enum OverrideSource {

        // higher priority wins
        PLUGIN_API(1), USER_API(2);

        private final int priority;

        private OverrideSource(int priority) {
            this.priority = priority;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy