org.glowroot.transaction.model.Transaction.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.transaction.model.Transaction.java

Source

/*
 * 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.List;
import java.util.Map;

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

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

import org.glowroot.api.ErrorMessage;
import org.glowroot.api.MessageSupplier;
import org.glowroot.api.TimerName;
import org.glowroot.api.internal.ReadableErrorMessage;
import org.glowroot.api.internal.ReadableMessage;
import org.glowroot.common.ScheduledRunnable;
import org.glowroot.jvm.ThreadAllocatedBytes;

import static com.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;

    // 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 boolean explicitSetTransactionType;

    private volatile String transactionName;
    private volatile boolean explicitSetTransactionName;

    private volatile @Nullable String user;

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

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

    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;

    // 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 traceStoreThresholdMillisOverride = USE_GENERAL_STORE_THRESHOLD;

    // 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 volatile boolean willBeStored;

    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, @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, ticker);
        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.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<String, String> getCustomAttributes() {
        if (customAttributes == null) {
            return ImmutableSetMultimap.of();
        }
        SetMultimap<String, String> orderedCustomAttributes = TreeMultimap.create(String.CASE_INSENSITIVE_ORDER,
                String.CASE_INSENSITIVE_ORDER);
        synchronized (customAttributes) {
            orderedCustomAttributes.putAll(customAttributes);
        }
        return ImmutableSetMultimap.copyOf(orderedCustomAttributes);
    }

    public Map<String, ? extends /*@Nullable*/Object> getCustomDetail() {
        MessageSupplier messageSupplier = traceEntryComponent.getRootEntry().getMessageSupplier();
        // root trace entry messageSupplier is never be null
        checkNotNull(messageSupplier);
        return ((ReadableMessage) messageSupplier.get()).getDetail();
    }

    public @Nullable ReadableErrorMessage 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 (ReadableErrorMessage) 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<GcInfo> getGcInfos() {
        return gcInfoComponent == null ? null : gcInfoComponent.getGcInfos();
    }

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

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

    public List<TraceEntry> getEntriesCopy() {
        readMemoryBarrier();
        return traceEntryComponent.getEntriesCopy();
    }

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

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

    public int getTraceStoreThresholdMillisOverride() {
        return traceStoreThresholdMillisOverride;
    }

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

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

    public boolean isPartiallyStored() {
        return partiallyStored;
    }

    public boolean willBeStored() {
        return willBeStored;
    }

    public long getThreadId() {
        return threadId;
    }

    public void setTransactionType(@Nullable String transactionType) {
        // use the first explicit, non-null/non-empty call to setTransactionType()
        if (!explicitSetTransactionType && transactionType != null && !transactionType.isEmpty()) {
            this.transactionType = transactionType;
            explicitSetTransactionType = true;
        }
    }

    public void setTransactionName(@Nullable String transactionName) {
        // use the first explicit, non-null/non-empty call to setTransactionName()
        if (!explicitSetTransactionName && transactionName != null && !transactionName.isEmpty()) {
            this.transactionName = transactionName;
            explicitSetTransactionName = true;
        }
    }

    public void setUser(String user) {
        // use the first non-null/non-empty user
        if (this.user == null) {
            this.user = user;
        }
    }

    public void putCustomAttribute(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) {
            customAttributes.put(name, val);
        }
    }

    public void setError(ErrorMessage errorMessage) {
        if (this.errorMessage == null) {
            // first call to this method for this trace
            this.errorMessage = errorMessage;
        }
    }

    public void setTraceStoreThresholdMillisOverride(int traceStoreThresholdMillisOverride) {
        if (this.traceStoreThresholdMillisOverride == -1) {
            // first call to this method for this trace, this is normal case
            this.traceStoreThresholdMillisOverride = traceStoreThresholdMillisOverride;
        } else {
            // use the minimum threshold passed to this method
            this.traceStoreThresholdMillisOverride = Math.min(this.traceStoreThresholdMillisOverride,
                    traceStoreThresholdMillisOverride);
        }
    }

    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 void setWillBeStored() {
        willBeStored = true;
    }

    public TraceEntry pushEntry(long startTick, MessageSupplier messageSupplier, TimerImpl timer) {
        return traceEntryComponent.pushEntry(startTick, messageSupplier, timer);
    }

    public TraceEntry addEntry(long startTick, long endTick, @Nullable MessageSupplier messageSupplier,
            @Nullable ErrorMessage errorMessage, boolean limitBypassed) {
        TraceEntry 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(TraceEntry 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 boolean readMemoryBarrier() {
        return memoryBarrier;
    }

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