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

org.glowroot.agent.impl.Transaction Maven / Gradle / Ivy

There is a newer version: 0.14.0-beta.3
Show newest version
/*
 * Copyright 2011-2019 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.agent.impl;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.javax.annotation.concurrent.GuardedBy;

import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.annotations.VisibleForTesting;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Strings;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Ticker;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ArrayListMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.HashMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ImmutableList;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ImmutableListMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ImmutableSetMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Iterables;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ListMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Lists;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Maps;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.SetMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Sets;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.TreeMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.io.BaseEncoding;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.Nullable;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.glowroot.agent.shaded.org.glowroot.agent.shaded.org.slf4j.Logger;
import org.glowroot.agent.shaded.org.glowroot.agent.shaded.org.slf4j.LoggerFactory;

import org.glowroot.agent.bytecode.api.ThreadContextThreadLocal;
import org.glowroot.agent.config.ConfigService;
import org.glowroot.agent.model.AggregatedTimer;
import org.glowroot.agent.model.AsyncQueryData;
import org.glowroot.agent.model.AsyncTimer;
import org.glowroot.agent.model.ErrorMessage;
import org.glowroot.agent.model.MergedThreadTimer;
import org.glowroot.agent.model.QueryCollector;
import org.glowroot.agent.model.ServiceCallCollector;
import org.glowroot.agent.model.SharedQueryTextCollection;
import org.glowroot.agent.model.ThreadProfile;
import org.glowroot.agent.model.ThreadStats;
import org.glowroot.agent.model.TransactionTimer;
import org.glowroot.agent.plugin.api.Message;
import org.glowroot.agent.plugin.api.MessageSupplier;
import org.glowroot.agent.plugin.api.ThreadContext.ServletRequestInfo;
import org.glowroot.agent.plugin.api.TimerName;
import org.glowroot.agent.plugin.api.internal.ReadableMessage;
import org.glowroot.agent.util.IterableWithSelfRemovableEntries.SelfRemovableEntry;
import org.glowroot.agent.util.ThreadAllocatedBytes;
import org.glowroot.agent.shaded.org.glowroot.common.config.AdvancedConfig;
import org.glowroot.agent.shaded.org.glowroot.common.util.Cancellable;
import org.glowroot.agent.shaded.org.glowroot.common.util.NotAvailableAware;
import org.glowroot.agent.shaded.org.glowroot.common.util.Traverser;
import org.glowroot.agent.shaded.org.glowroot.wire.api.model.AggregateOuterClass.Aggregate;
import org.glowroot.agent.shaded.org.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.agent.shaded.org.glowroot.wire.api.model.TraceOuterClass.Trace;

import static org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.agent.util.Checkers.castInitialized;

// 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);

    static final int USE_GENERAL_STORE_THRESHOLD = -1;

    static final String AUXILIARY_THREAD_MESSAGE = "auxiliary thread";

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

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

    // this is only to limit memory
    private static final int TRANSACTION_AUX_THREAD_CONTEXT_LIMIT =
            Integer.getInteger("glowroot.transaction.aux.thread.context.limit", 1000);

    private static final Random random = new Random();

    private volatile @Nullable String traceId;

    private final long startTime;
    private final long startTick;

    private volatile boolean async;
    private volatile boolean outer;

    private volatile String transactionType;
    private volatile int transactionTypePriority = Integer.MIN_VALUE;

    private volatile String transactionName;
    private volatile int transactionNamePriority = Integer.MIN_VALUE;

    private volatile @Nullable String user;
    private volatile int userPriority = Integer.MIN_VALUE;

    private final Object attributesLock = new Object();
    // lazy loaded to reduce memory when custom attributes are not used
    @GuardedBy("attributesLock")
    private @MonotonicNonNull SetMultimap attributes;

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

    private final int maxTraceEntries;
    private final int maxQueryAggregates;
    private final int maxServiceCallAggregates;
    private final int maxProfileSamples;

    private final TransactionRegistry transactionRegistry;
    private final TransactionService transactionService;
    private final ConfigService configService;

    // stack trace data constructed from profiling
    private volatile @MonotonicNonNull ThreadProfile mainThreadProfile;
    private volatile @MonotonicNonNull ThreadProfile auxThreadProfile;

    // overrides general store threshold
    // -1 means don't override the general store threshold
    private volatile int slowThresholdMillis = USE_GENERAL_STORE_THRESHOLD;
    private volatile int slowThresholdMillisPriority = Integer.MIN_VALUE;

    // this is stored in the trace so it is only scheduled a single time, and also so it can be
    // canceled at trace completion
    private volatile @MonotonicNonNull Cancellable immedateTraceStoreRunnable;

    private volatile boolean partiallyStored;

    private volatile 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;

    // ideally would use AtomicInteger here, but using plain volatile int as optimization since
    // it's ok if race condition in limit check
    private volatile int entryLimitCounter;
    private volatile int extraErrorEntryLimitCounter;

    private volatile @Nullable AtomicInteger throwableFrameLimitCounter;

    private final ThreadContextImpl mainThreadContext;

    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull List auxThreadContexts;
    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull List unmergeableAuxThreadContexts;
    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull Set unmergedLimitExceededAuxThreadContexts;

    private final Object asyncComponentsInitLock = new Object();
    private volatile @MonotonicNonNull AsyncComponents asyncComponents;

    private final Object sharedQueryTextCollectionLock = new Object();
    @GuardedBy("sharedQueryTextCollectionLock")
    private @MonotonicNonNull SharedQueryTextCollectionImpl sharedQueryTextCollection;

    private volatile boolean waitingToEndAsync;
    private volatile boolean completed;
    private volatile long endTick;

    private final Ticker ticker;

    private @Nullable SelfRemovableEntry transactionEntry;

    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull RootTimerCollectorImpl alreadyMergedAuxThreadTimers;
    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull ThreadStatsCollectorImpl alreadyMergedAuxThreadStats;
    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull QueryCollector alreadyMergedAuxQueries;
    @GuardedBy("mainThreadContext")
    private @MonotonicNonNull ServiceCallCollector alreadyMergedAuxServiceCalls;
    @GuardedBy("mainThreadContext")
    private boolean stopMergingAuxThreadContexts;

    Transaction(long startTime, long startTick, String transactionType, String transactionName,
            MessageSupplier messageSupplier, TimerName timerName, boolean captureThreadStats,
            int maxTraceEntries, int maxQueryAggregates, int maxServiceCallAggregates,
            int maxProfileSamples, @Nullable ThreadAllocatedBytes threadAllocatedBytes,
            CompletionCallback completionCallback, Ticker ticker,
            TransactionRegistry transactionRegistry, TransactionService transactionService,
            ConfigService configService, ThreadContextThreadLocal.Holder threadContextHolder,
            int rootNestingGroupId, int rootSuppressionKeyId) {
        this.startTime = startTime;
        this.startTick = startTick;
        this.transactionType = transactionType;
        this.transactionName = transactionName;
        this.maxTraceEntries = maxTraceEntries;
        this.maxQueryAggregates = maxQueryAggregates;
        this.maxServiceCallAggregates = maxServiceCallAggregates;
        this.maxProfileSamples = maxProfileSamples;
        this.completionCallback = completionCallback;
        this.ticker = ticker;
        this.transactionRegistry = transactionRegistry;
        this.transactionService = transactionService;
        this.configService = configService;
        mainThreadContext = new ThreadContextImpl(castInitialized(this), null, null,
                messageSupplier, timerName, startTick, captureThreadStats, maxQueryAggregates,
                maxServiceCallAggregates, threadAllocatedBytes, false, ticker, threadContextHolder,
                null, rootNestingGroupId, rootSuppressionKeyId);
    }

    long getStartTime() {
        return startTime;
    }

    public String getTraceId() {
        if (traceId == null) {
            // double-checked locking works here because traceId is volatile
            //
            // synchronized on "this" as a micro-optimization just so don't need to create an empty
            // object to lock on
            synchronized (this) {
                if (traceId == null) {
                    traceId = buildTraceId(startTime);
                }
            }
        }
        return traceId;
    }

    public long getStartTick() {
        return startTick;
    }

    public boolean isCompleted() {
        return completed;
    }

    public boolean isFullyCompleted() {
        return completed && captureTime != 0;
    }

    long getEndTick() {
        return endTick;
    }

    public long getDurationNanos() {
        return completed ? endTick - startTick : ticker.read() - startTick;
    }

    public String getTransactionType() {
        return transactionType;
    }

    public String getTransactionName() {
        return transactionName;
    }

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

    public String getUser() {
        return Strings.nullToEmpty(user);
    }

    public SetMultimap getAttributes() {
        synchronized (attributesLock) {
            if (attributes == null) {
                return ImmutableSetMultimap.of();
            }
            SetMultimap orderedAttributes = TreeMultimap.create();
            orderedAttributes.putAll(attributes);
            return orderedAttributes;
        }
    }

    Map getDetail() {
        Object messageSupplier = mainThreadContext.getRootEntry().getMessageSupplier();
        // root trace entry messageSupplier is never null
        checkNotNull(messageSupplier);
        // root trace entry messageSupplier is never QueryMessageSupplier
        return ((ReadableMessage) ((MessageSupplier) messageSupplier).get()).getDetail();
    }

    @Nullable
    List getLocationStackTrace() {
        return mainThreadContext.getRootEntry().getLocationStackTrace();
    }

    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 mainThreadContext.getRootEntry().getErrorMessage();
    }

    boolean isAsync() {
        return async;
    }

    boolean isOuter() {
        return outer;
    }

    TimerImpl getMainThreadRootTimer() {
        memoryBarrierRead();
        return mainThreadContext.getRootTimer();
    }

    boolean hasAuxThreadContexts() {
        synchronized (mainThreadContext) {
            return auxThreadContexts != null;
        }
    }

    void mergeAuxThreadTimersInto(AggregatedTimer rootAuxThreadTimer) {
        synchronized (mainThreadContext) {
            if (auxThreadContexts == null) {
                return;
            }
            if (alreadyMergedAuxThreadTimers != null) {
                for (MergedThreadTimer rootTimer : alreadyMergedAuxThreadTimers.getRootTimers()) {
                    rootAuxThreadTimer.addDataFrom(rootTimer);
                }
            }
            for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                rootAuxThreadTimer.addDataFrom(auxThreadContext.getRootTimer());
            }
        }
    }

    boolean hasAsyncTimers() {
        return asyncComponents != null;
    }

    void mergeAsyncTimersInto(RootTimerCollector asyncTimers) {
        if (asyncComponents != null) {
            asyncComponents.mergeAsyncTimersInto(asyncTimers);
        }
    }

    // can be called from a non-transaction thread
    ThreadStats getMainThreadStats() {
        return mainThreadContext.getThreadStats();
    }

    public long getCpuNanos() {
        long cpuNanos = mainThreadContext.getCpuNanos();
        synchronized (mainThreadContext) {
            if (auxThreadContexts == null) {
                return cpuNanos;
            }
            if (alreadyMergedAuxThreadStats != null) {
                cpuNanos =
                        NotAvailableAware.add(cpuNanos, alreadyMergedAuxThreadStats.getCpuNanos());
            }
            for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                cpuNanos =
                        NotAvailableAware.add(cpuNanos, auxThreadContext.getCpuNanos());
            }
        }
        return cpuNanos;
    }

    void mergeAuxThreadStatsInto(ThreadStatsCollector collector) {
        synchronized (mainThreadContext) {
            if (auxThreadContexts == null) {
                return;
            }
            if (alreadyMergedAuxThreadStats != null) {
                collector.mergeThreadStats(alreadyMergedAuxThreadStats.getMergedThreadStats());
            }
            for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                collector.mergeThreadStats(auxThreadContext.getThreadStats());
            }
        }
    }

    void mergeQueriesInto(QueryCollector collector) {
        memoryBarrierRead();
        mainThreadContext.mergeQueriesInto(collector);
        synchronized (mainThreadContext) {
            if (auxThreadContexts != null) {
                if (alreadyMergedAuxQueries != null) {
                    alreadyMergedAuxQueries.mergeQueriesInto(collector);
                }
                for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                    auxThreadContext.mergeQueriesInto(collector);
                }
            }
        }
        if (asyncComponents != null) {
            asyncComponents.mergeQueriesInto(collector);
        }
    }

    public List getQueries() {
        synchronized (sharedQueryTextCollectionLock) {
            if (sharedQueryTextCollection == null) {
                sharedQueryTextCollection = new SharedQueryTextCollectionImpl();
            }
            return getQueriesInternal(sharedQueryTextCollection);
        }
    }

    int getQueryCount() {
        return getQueriesInternal(new NopSharedQueryTextCollection()).size();
    }

    private List getQueriesInternal(
            SharedQueryTextCollection sharedQueryTextCollection) {
        QueryCollector collector = new QueryCollector(maxQueryAggregates,
                AdvancedConfig.OVERALL_AGGREGATE_QUERIES_HARD_LIMIT_MULTIPLIER);
        mergeQueriesInto(collector);
        return collector.toAggregateProto(sharedQueryTextCollection, true);
    }

    public List getSharedQueryTexts() {
        synchronized (sharedQueryTextCollectionLock) {
            if (sharedQueryTextCollection == null) {
                sharedQueryTextCollection = new SharedQueryTextCollectionImpl();
            }
            return ImmutableList.copyOf(sharedQueryTextCollection.sharedQueryTexts);
        }
    }

    void mergeServiceCallsInto(ServiceCallCollector collector) {
        memoryBarrierRead();
        mainThreadContext.mergeServiceCallsInto(collector);
        synchronized (mainThreadContext) {
            if (auxThreadContexts != null) {
                if (alreadyMergedAuxServiceCalls != null) {
                    alreadyMergedAuxServiceCalls.mergeServiceCallsInto(collector);
                }
                for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                    auxThreadContext.mergeServiceCallsInto(collector);
                }
            }
        }
        if (asyncComponents != null) {
            asyncComponents.mergeServiceCallsInto(collector);
        }
    }

    // this method has side effect of incrementing counter
    boolean allowAnotherEntry() {
        return entryLimitCounter++ < maxTraceEntries;
    }

    // this method has side effect of incrementing counter
    boolean allowAnotherErrorEntry() {
        // use higher entry limit when adding errors, but still need some kind of cap
        return entryLimitCounter++ < maxTraceEntries
                || extraErrorEntryLimitCounter++ < maxTraceEntries;
    }

    public void visitEntries(long captureTick, TraceEntryVisitor entryVisitor) {
        synchronized (sharedQueryTextCollectionLock) {
            if (sharedQueryTextCollection == null) {
                sharedQueryTextCollection = new SharedQueryTextCollectionImpl();
            }
            visitEntriesInternal(captureTick, entryVisitor, sharedQueryTextCollection);
        }
    }

    int getEntryCount(long captureTick) {
        CountingEntryVisitor entryVisitor = new CountingEntryVisitor();
        visitEntriesInternal(captureTick, entryVisitor, new NopSharedQueryTextCollection());
        return entryVisitor.count;
    }

    private void visitEntriesInternal(long captureTick, TraceEntryVisitor entryVisitor,
            SharedQueryTextCollection sharedQueryTextCollection) {
        memoryBarrierRead();
        ListMultimap priorEntryChildThreadContextMap =
                buildPriorEntryChildThreadContextMap();
        ListMultimap parentChildMap = ArrayListMultimap.create();
        mainThreadContext.populateParentChildMap(parentChildMap, captureTick,
                priorEntryChildThreadContextMap);
        synchronized (mainThreadContext) {
            if (auxThreadContexts != null) {
                for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                    auxThreadContext.populateParentChildMap(parentChildMap, captureTick,
                            priorEntryChildThreadContextMap);
                }
            }
        }
        new ParentChildMapTrimmer(mainThreadContext.getRootEntry(), parentChildMap, captureTick)
                .traverse();
        addProtobufChildEntries(mainThreadContext.getRootEntry(), parentChildMap, startTick,
                captureTick, 0, entryVisitor, sharedQueryTextCollection, async);
    }

    long getMainThreadProfileSampleCount() {
        if (mainThreadProfile == null) {
            return 0;
        } else {
            return mainThreadProfile.getSampleCount();
        }
    }

    @Nullable
    ThreadProfile getMainThreadProfile() {
        return mainThreadProfile;
    }

    public @Nullable Profile getMainThreadProfileProtobuf() {
        if (mainThreadProfile == null) {
            return null;
        }
        return mainThreadProfile.toProto();
    }

    boolean isMainThreadProfileSampleLimitExceeded(long profileSampleCount) {
        return profileSampleCount >= maxProfileSamples && mainThreadProfile != null
                && mainThreadProfile.isSampleLimitExceeded();
    }

    long getAuxThreadProfileSampleCount() {
        if (auxThreadProfile == null) {
            return 0;
        } else {
            return auxThreadProfile.getSampleCount();
        }
    }

    @Nullable
    ThreadProfile getAuxThreadProfile() {
        return auxThreadProfile;
    }

    public @Nullable Profile getAuxThreadProfileProtobuf() {
        if (auxThreadProfile == null) {
            return null;
        }
        return auxThreadProfile.toProto();
    }

    boolean isAuxThreadProfileSampleLimitExceeded(long profileSampleCount) {
        return profileSampleCount >= maxProfileSamples && auxThreadProfile != null
                && auxThreadProfile.isSampleLimitExceeded();
    }

    int getSlowThresholdMillisOverride() {
        return slowThresholdMillis;
    }

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

    public boolean isPartiallyStored() {
        return partiallyStored;
    }

    public ThreadContextImpl getMainThreadContext() {
        return mainThreadContext;
    }

    public List getActiveAuxThreadContexts() {
        synchronized (mainThreadContext) {
            if (auxThreadContexts == null) {
                return ImmutableList.of();
            }
            List activeAuxThreadContexts = Lists.newArrayList();
            for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                if (auxThreadContext.isActive()) {
                    activeAuxThreadContexts.add(auxThreadContext);
                }
            }
            return activeAuxThreadContexts;
        }
    }

    void setAsync() {
        async = true;
    }

    void setOuter() {
        outer = true;
    }

    void setTransactionType(String transactionType, int priority) {
        if (priority > transactionTypePriority && !transactionType.isEmpty()) {
            this.transactionType = transactionType;
            transactionTypePriority = priority;
        }
    }

    void setTransactionName(String transactionName, int priority) {
        if (priority > transactionNamePriority && !transactionName.isEmpty()) {
            this.transactionName = transactionName;
            transactionNamePriority = priority;
        }
    }

    void setUser(String user, int priority) {
        if (priority > userPriority && !user.isEmpty()) {
            this.user = user;
            userPriority = priority;
        }
    }

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

    void setError(@Nullable String message, @Nullable Throwable t) {
        if (this.errorMessage == null) {
            this.errorMessage = ErrorMessage.create(message, t, getThrowableFrameLimitCounter());
        }
    }

    void setSlowThresholdMillis(int slowThresholdMillis, int priority) {
        if (priority > slowThresholdMillisPriority) {
            this.slowThresholdMillis = slowThresholdMillis;
            slowThresholdMillisPriority = priority;
        } else if (priority == slowThresholdMillisPriority) {
            // use the minimum threshold from the same override source
            this.slowThresholdMillis = Math.min(this.slowThresholdMillis, slowThresholdMillis);
        }
    }

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

    void setPartiallyStored() {
        partiallyStored = true;
    }

    void setTransactionEntry(SelfRemovableEntry transactionEntry) {
        this.transactionEntry = transactionEntry;
    }

    void removeFromActiveTransactions() {
        checkNotNull(transactionEntry).remove();
    }

    @Nullable
    ThreadContextImpl startAuxThreadContext(@Nullable TraceEntryImpl parentTraceEntry,
            @Nullable TraceEntryImpl parentThreadContextPriorEntry, TimerName auxTimerName,
            long startTick, ThreadContextThreadLocal.Holder threadContextHolder,
            @Nullable ServletRequestInfo servletRequestInfo,
            @Nullable ThreadAllocatedBytes threadAllocatedBytes) {
        ThreadContextImpl auxThreadContext;
        synchronized (mainThreadContext) {
            // check completed and add aux thread context inside synchronized block to avoid race
            // condition with setting completed and detaching incomplete aux thread contexts, see
            // synchronized block in end()
            if (completed) {
                return null;
            }
            if (auxThreadContexts == null) {
                auxThreadContexts = Lists.newArrayList();
            }
            // conditions below for parentTraceEntry and parentThreadContextPriorEntry are redundant
            // since they will not be null until after allowAnotherAuxThreadContextWithHierarchy()
            // starts returning false
            if (allowAnotherAuxThreadContextWithTraceEntries() && parentTraceEntry != null
                    && parentThreadContextPriorEntry != null) {
                auxThreadContext = new ThreadContextImpl(this, parentTraceEntry,
                        parentThreadContextPriorEntry, AuxThreadRootMessageSupplier.INSTANCE,
                        auxTimerName, startTick, mainThreadContext.getCaptureThreadStats(),
                        maxQueryAggregates, maxServiceCallAggregates, threadAllocatedBytes, false,
                        ticker, threadContextHolder, servletRequestInfo, 0, 0);
                auxThreadContexts.add(auxThreadContext);
            } else {
                auxThreadContext = new ThreadContextImpl(this, mainThreadContext.getRootEntry(),
                        mainThreadContext.getTailEntry(), AuxThreadRootMessageSupplier.INSTANCE,
                        auxTimerName, startTick, mainThreadContext.getCaptureThreadStats(),
                        maxQueryAggregates, maxServiceCallAggregates, threadAllocatedBytes, true,
                        ticker, threadContextHolder, servletRequestInfo, 0, 0);
                if (unmergedLimitExceededAuxThreadContexts == null) {
                    unmergedLimitExceededAuxThreadContexts = Sets.newHashSet();
                }
                unmergedLimitExceededAuxThreadContexts.add(auxThreadContext);
            }
        }
        // see counterpart to this synchronization (and explanation) in ThreadContextImpl.detach()
        synchronized (threadContextHolder) {
            threadContextHolder.set(auxThreadContext);
        }
        return auxThreadContext;
    }

    void mergeLimitExceededAuxThreadContext(ThreadContextImpl auxThreadContext) {
        synchronized (mainThreadContext) {
            checkNotNull(unmergedLimitExceededAuxThreadContexts).remove(auxThreadContext);
            if (auxThreadContext.hasTraceEntries()) {
                checkNotNull(auxThreadContexts).add(auxThreadContext);
                return;
            }
            initAlreadyMergedAuxComponentsIfNeeded();
            mergeAux(auxThreadContext);
        }
    }

    AsyncTimer startAsyncTimer(TimerName asyncTimerName, long startTick) {
        return getOrInitAsyncComponents().startAsyncTimer(asyncTimerName, startTick);
    }

    AsyncQueryData getOrCreateAsyncQueryData(String queryType, String queryText,
            boolean bypassLimit) {
        return getOrInitAsyncComponents().getOrCreateAsyncQueryData(queryType, queryText,
                bypassLimit);
    }

    AsyncQueryData getOrCreateAsyncServiceCallData(String serviceCallType, String serviceCallText,
            boolean bypassLimit) {
        return getOrInitAsyncComponents().getOrCreateAsyncServiceCallData(serviceCallType,
                serviceCallText, bypassLimit);
    }

    TraceEntryImpl startInnerTransaction(String transactionType, String transactionName,
            MessageSupplier messageSupplier, TimerName timerName,
            ThreadContextThreadLocal.Holder threadContextHolder, int rootNestingGroupId,
            int rootSuppressionKeyId) {
        return transactionService.startTransaction(transactionType, transactionName,
                messageSupplier, timerName, threadContextHolder, rootNestingGroupId,
                rootSuppressionKeyId);
    }

    boolean isEntryLimitExceeded(int entryCount) {
        return entryCount >= maxTraceEntries && entryLimitCounter > maxTraceEntries;
    }

    boolean isQueryLimitExceeded(int queryCount) {
        // "LIMIT EXCEEDED BUCKET" will always push query count over the max
        return queryCount > maxQueryAggregates;
    }

    void captureStackTrace(boolean auxiliary, ThreadInfo threadInfo) {
        if (completed) {
            return;
        }
        ThreadProfile profile;
        if (auxiliary) {
            profile = auxThreadProfile;
        } else {
            profile = mainThreadProfile;
        }
        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 = new ThreadProfile(maxProfileSamples);
            profile.addStackTrace(threadInfo);
            if (auxiliary) {
                auxThreadProfile = profile;
            } else {
                mainThreadProfile = profile;
            }
            return;
        }
        profile.addStackTrace(threadInfo);
    }

    void setWaitingToEndAsync() {
        waitingToEndAsync = true;
    }

    void end(long endTick, boolean completeAsyncTransaction,
            boolean isSetTransactionAsyncComplete) {
        if (async && (!completeAsyncTransaction
                || waitingToEndAsync && isSetTransactionAsyncComplete)) {
            return;
        }
        synchronized (mainThreadContext) {
            if (completed) {
                // protect against plugin calling setTransactionAsyncComplete() multiple times,
                // potentially from different threads (e.g. netty plugin ending transaction by
                // sending LastHttpContent at the same time client disconnects)
                return;
            }
            // set endTick first before completed, to avoid race condition in getDurationNanos()
            this.endTick = endTick;
            // set completed and detach incomplete aux thread contexts inside synchronized block
            // to avoid race condition with adding new aux thread contexts, see synchronized block
            // in startAuxThreadContext()
            completed = true;
            detachIncompleteAuxThreadContexts();
        }
        if (immedateTraceStoreRunnable != null) {
            immedateTraceStoreRunnable.cancel();
        }
        completionCallback.completed(this);
    }

    void setCaptureTime(long captureTime) {
        this.captureTime = captureTime;
    }

    public long getCaptureTime() {
        return captureTime;
    }

    TransactionRegistry getTransactionRegistry() {
        return transactionRegistry;
    }

    TransactionService getTransactionService() {
        return transactionService;
    }

    ConfigService getConfigService() {
        return configService;
    }

    AtomicInteger getThrowableFrameLimitCounter() {
        if (throwableFrameLimitCounter == null) {
            // double-checked locking works here because throwableFrameLimitCounter is volatile
            //
            // synchronized on "this" as a micro-optimization just so don't need to create an empty
            // object to lock on
            synchronized (this) {
                if (throwableFrameLimitCounter == null) {
                    throwableFrameLimitCounter = new AtomicInteger();
                }
            }
        }
        return throwableFrameLimitCounter;
    }

    boolean memoryBarrierRead() {
        return memoryBarrier;
    }

    void memoryBarrierWrite() {
        memoryBarrier = true;
    }

    void memoryBarrierReadWrite() {
        memoryBarrierRead();
        memoryBarrierWrite();
    }

    @GuardedBy("mainThreadContext")
    @RequiresNonNull("auxThreadContexts")
    private boolean allowAnotherAuxThreadContextWithTraceEntries() {
        int unmergedCount = auxThreadContexts.size();
        if (unmergeableAuxThreadContexts != null) {
            unmergedCount += unmergeableAuxThreadContexts.size();
        }
        if (unmergedCount < TRANSACTION_AUX_THREAD_CONTEXT_LIMIT) {
            return true;
        }
        if (stopMergingAuxThreadContexts) {
            return false;
        }
        List mergeableAuxThreadContexts = Lists.newArrayList();
        List unmergeableAuxThreadContexts = Lists.newArrayList();
        List mergeableButIncompleteAuxThreadContexts = Lists.newArrayList();
        for (ThreadContextImpl auxThreadContext : auxThreadContexts) {
            if (!auxThreadContext.isMergeable()) {
                // once it's not mergeable it will never be mergeable
                unmergeableAuxThreadContexts.add(auxThreadContext);
            } else if (auxThreadContext.isCompleted()) {
                mergeableAuxThreadContexts.add(auxThreadContext);
            } else {
                mergeableButIncompleteAuxThreadContexts.add(auxThreadContext);
            }
        }
        if (mergeableAuxThreadContexts.size() < 0.01 * TRANSACTION_AUX_THREAD_CONTEXT_LIMIT) {
            // not worth continuing to merge
            stopMergingAuxThreadContexts = true;
            return false;
        }
        initAlreadyMergedAuxComponentsIfNeeded();
        for (ThreadContextImpl mergeableAuxThreadContext : mergeableAuxThreadContexts) {
            mergeAux(mergeableAuxThreadContext);
        }
        if (this.unmergeableAuxThreadContexts == null) {
            this.unmergeableAuxThreadContexts = Lists.newArrayList(unmergeableAuxThreadContexts);
        } else {
            this.unmergeableAuxThreadContexts.addAll(unmergeableAuxThreadContexts);
        }
        auxThreadContexts = Lists.newArrayList(mergeableButIncompleteAuxThreadContexts);
        return true;
    }

    @GuardedBy("mainThreadContext")
    @EnsuresNonNull({"alreadyMergedAuxThreadTimers", "alreadyMergedAuxThreadStats",
            "alreadyMergedAuxQueries", "alreadyMergedAuxServiceCalls"})
    private void initAlreadyMergedAuxComponentsIfNeeded() {
        if (alreadyMergedAuxThreadTimers == null) {
            alreadyMergedAuxThreadTimers = new RootTimerCollectorImpl();
        }
        if (alreadyMergedAuxThreadStats == null) {
            alreadyMergedAuxThreadStats = new ThreadStatsCollectorImpl();
        }
        if (alreadyMergedAuxQueries == null) {
            alreadyMergedAuxQueries = new QueryCollector(maxQueryAggregates,
                    AdvancedConfig.OVERALL_AGGREGATE_QUERIES_HARD_LIMIT_MULTIPLIER);
        }
        if (alreadyMergedAuxServiceCalls == null) {
            alreadyMergedAuxServiceCalls = new ServiceCallCollector(maxServiceCallAggregates,
                    AdvancedConfig.OVERALL_AGGREGATE_QUERIES_HARD_LIMIT_MULTIPLIER);
        }
    }

    @GuardedBy("mainThreadContext")
    @RequiresNonNull({"alreadyMergedAuxThreadTimers", "alreadyMergedAuxThreadStats",
            "alreadyMergedAuxQueries", "alreadyMergedAuxServiceCalls"})
    private void mergeAux(ThreadContextImpl mergeableAuxThreadContext) {
        alreadyMergedAuxThreadTimers.mergeRootTimer(mergeableAuxThreadContext.getRootTimer());
        alreadyMergedAuxThreadStats.mergeThreadStats(mergeableAuxThreadContext.getThreadStats());
        mergeableAuxThreadContext.mergeQueriesInto(alreadyMergedAuxQueries);
        mergeableAuxThreadContext.mergeServiceCallsInto(alreadyMergedAuxServiceCalls);
    }

    private AsyncComponents getOrInitAsyncComponents() {
        if (asyncComponents == null) {
            synchronized (asyncComponentsInitLock) {
                if (asyncComponents == null) {
                    asyncComponents = new AsyncComponents(maxQueryAggregates,
                            maxServiceCallAggregates, ticker);
                }
            }
        }
        return asyncComponents;
    }

    private static void addProtobufChildEntries(TraceEntryImpl entry,
            ListMultimap parentChildMap, long transactionStartTick,
            long captureTick, int depth, TraceEntryVisitor entryVisitor,
            SharedQueryTextCollection sharedQueryTextCollection, boolean removeSingleAuxEntry) {
        if (!parentChildMap.containsKey(entry)) {
            // check containsKey to avoid creating garbage empty list via ListMultimap
            return;
        }
        Collection childEntries = parentChildMap.get(entry);
        for (TraceEntryImpl childEntry : childEntries) {
            boolean singleAuxEntry = childEntries.size() == 1 && childEntry.isAuxThreadRoot()
                    && !childEntry.hasLocationStackTrace();
            if (singleAuxEntry && removeSingleAuxEntry) {
                addProtobufChildEntries(childEntry, parentChildMap, transactionStartTick,
                        captureTick, depth, entryVisitor, sharedQueryTextCollection,
                        removeSingleAuxEntry);
            } else {
                childEntry.accept(depth, transactionStartTick, captureTick, entryVisitor,
                        sharedQueryTextCollection);
                addProtobufChildEntries(childEntry, parentChildMap, transactionStartTick,
                        captureTick, depth + 1, entryVisitor, sharedQueryTextCollection, false);
            }
        }
    }

    private ListMultimap buildPriorEntryChildThreadContextMap() {
        synchronized (mainThreadContext) {
            if (auxThreadContexts == null) {
                return ImmutableListMultimap.of();
            }
            ListMultimap parentChildMap =
                    ArrayListMultimap.create();
            for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
                // checkNotNull is safe b/c aux thread contexts have non-null parent thread context
                // prior entries when they are not limit exceeded aux thread contexts
                parentChildMap.put(
                        checkNotNull(auxThreadContext.getParentThreadContextPriorEntry()),
                        auxThreadContext);
            }
            return parentChildMap;
        }
    }

    @GuardedBy("mainThreadContext")
    private void detachIncompleteAuxThreadContexts() {
        if (auxThreadContexts == null) {
            return;
        }
        for (ThreadContextImpl auxThreadContext : getUnmergedAuxThreadContext()) {
            if (auxThreadContext.isCompleted()) {
                continue;
            }
            auxThreadContext.detach();
            if (!logger.isDebugEnabled()) {
                continue;
            }
            ThreadInfo threadInfo = ManagementFactory.getThreadMXBean()
                    .getThreadInfo(auxThreadContext.getThreadId(), Integer.MAX_VALUE);
            if (logger.isDebugEnabled() && !isCompleted() && threadInfo != null) {
                // still not complete and got a valid stack trace from auxiliary thread
                StringBuilder sb = new StringBuilder();
                for (StackTraceElement stackTraceElement : threadInfo.getStackTrace()) {
                    sb.append("    ");
                    sb.append(stackTraceElement.toString());
                    sb.append('\n');
                }
                logger.debug("auxiliary thread extended beyond the transaction which started it\n"
                        + "{}", sb);
            }
        }
    }

    @GuardedBy("mainThreadContext")
    @RequiresNonNull("auxThreadContexts")
    private Iterable getUnmergedAuxThreadContext() {
        if (unmergeableAuxThreadContexts == null) {
            if (unmergedLimitExceededAuxThreadContexts == null) {
                return auxThreadContexts;
            } else {
                return Iterables.concat(auxThreadContexts, unmergedLimitExceededAuxThreadContexts);
            }
        } else if (unmergedLimitExceededAuxThreadContexts == null) {
            return Iterables.concat(unmergeableAuxThreadContexts, auxThreadContexts);
        } else {
            return Iterables.concat(unmergeableAuxThreadContexts, auxThreadContexts,
                    unmergedLimitExceededAuxThreadContexts);
        }
    }

    @VisibleForTesting
    static String buildTraceId(long startTime) {
        byte[] bytes = new byte[10];
        random.nextBytes(bytes);
        // lower 6 bytes of current time will wrap only every 8925 years
        return lowerSixBytesHex(startTime) + BaseEncoding.base16().lowerCase().encode(bytes);
    }

    @VisibleForTesting
    static String lowerSixBytesHex(long startTime) {
        long mask = 1L << 48;
        return Long.toHexString(mask | (startTime & (mask - 1))).substring(1);
    }

    interface CompletionCallback {
        void completed(Transaction transaction);
    }

    interface RootTimerCollector {
        void mergeRootTimer(TransactionTimer rootTimer);
    }

    interface ThreadStatsCollector {
        void mergeThreadStats(ThreadStats threadStats);
    }

    public interface TraceEntryVisitor {
        void visitEntry(Trace.Entry entry);
    }

    private static class AuxThreadRootMessageSupplier extends MessageSupplier {

        private static final AuxThreadRootMessageSupplier INSTANCE =
                new AuxThreadRootMessageSupplier();

        private final Message message = Message.create(AUXILIARY_THREAD_MESSAGE);

        @Override
        public Message get() {
            return message;
        }
    }

    private static class ParentChildMapTrimmer extends Traverser {

        private final ListMultimap parentChildMap;
        private final long captureTick;

        private ParentChildMapTrimmer(TraceEntryImpl rootNode,
                ListMultimap parentChildMap,
                long captureTick) {
            super(rootNode);
            this.parentChildMap = parentChildMap;
            this.captureTick = captureTick;
        }

        @Override
        public List visit(TraceEntryImpl node, int depth) {
            if (!parentChildMap.containsKey(node)) {
                // check containsKey to avoid creating garbage empty list via ListMultimap
                return ImmutableList.of();
            }
            return parentChildMap.get(node);
        }

        @Override
        public void revisitAfterChildren(TraceEntryImpl node) {
            if (!parentChildMap.containsKey(node)) {
                // check containsKey to avoid creating garbage empty list via ListMultimap
                return;
            }
            List childEntries = parentChildMap.get(node);
            ListIterator i = childEntries.listIterator();
            while (i.hasNext()) {
                TraceEntryImpl childEntry = i.next();
                if (childEntry.getStartTick() > captureTick) {
                    i.remove();
                }
            }
            i = childEntries.listIterator();
            while (i.hasNext()) {
                TraceEntryImpl childEntry = i.next();
                if (!childEntry.isAuxThreadRoot() || childEntry.hasLocationStackTrace()) {
                    continue;
                }
                if (!parentChildMap.containsKey(childEntry)) {
                    // check containsKey to avoid creating garbage empty list via ListMultimap
                    //
                    // remove empty aux thread root
                    i.remove();
                    continue;
                }
                List subChildEntries = parentChildMap.get(childEntry);
                if (subChildEntries.isEmpty()) {
                    // remove empty aux thread root
                    i.remove();
                    continue;
                }
                if (subChildEntries.size() == 1) {
                    TraceEntryImpl singleSubChildEntry = subChildEntries.get(0);
                    if (singleSubChildEntry.isAuxThreadRoot()) {
                        // compact consecutive single aux root entries
                        i.set(singleSubChildEntry);
                    }
                }
            }
        }
    }

    private static class CountingEntryVisitor implements TraceEntryVisitor {

        private int count;

        @Override
        public void visitEntry(Trace.Entry entry) {
            if (countEntry(entry)) {
                count++;
            }
        }

        private static boolean countEntry(Trace.Entry entry) {
            // don't count "auxiliary thread" entries since those are not counted in
            // maxTraceEntriesPerTransaction limit (and it's confusing when entry count exceeds the
            // limit)
            return !entry.getMessage().equals(Transaction.AUXILIARY_THREAD_MESSAGE);
        }
    }

    private static class SharedQueryTextCollectionImpl implements SharedQueryTextCollection {

        private final Map sharedQueryTextIndexes = Maps.newHashMap();
        private List sharedQueryTexts = Lists.newArrayList();

        @Override
        public int getSharedQueryTextIndex(String queryText) {
            Integer sharedQueryTextIndex = sharedQueryTextIndexes.get(queryText);
            if (sharedQueryTextIndex == null) {
                sharedQueryTextIndex = sharedQueryTextIndexes.size();
                sharedQueryTextIndexes.put(queryText, sharedQueryTextIndex);
                sharedQueryTexts.add(queryText);
            }
            return sharedQueryTextIndex;
        }
    }

    private static class NopSharedQueryTextCollection implements SharedQueryTextCollection {

        @Override
        public int getSharedQueryTextIndex(String queryText) {
            return 0;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy