/*
* Copyright 2016-2017 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.plugin.executor;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.glowroot.agent.plugin.api.Agent;
import org.glowroot.agent.plugin.api.AuxThreadContext;
import org.glowroot.agent.plugin.api.Logger;
import org.glowroot.agent.plugin.api.ThreadContext;
import org.glowroot.agent.plugin.api.Timer;
import org.glowroot.agent.plugin.api.TimerName;
import org.glowroot.agent.plugin.api.TraceEntry;
import org.glowroot.agent.plugin.api.weaving.BindClassMeta;
import org.glowroot.agent.plugin.api.weaving.BindParameter;
import org.glowroot.agent.plugin.api.weaving.BindReceiver;
import org.glowroot.agent.plugin.api.weaving.BindThrowable;
import org.glowroot.agent.plugin.api.weaving.BindTraveler;
import org.glowroot.agent.plugin.api.weaving.IsEnabled;
import org.glowroot.agent.plugin.api.weaving.Mixin;
import org.glowroot.agent.plugin.api.weaving.OnAfter;
import org.glowroot.agent.plugin.api.weaving.OnBefore;
import org.glowroot.agent.plugin.api.weaving.OnReturn;
import org.glowroot.agent.plugin.api.weaving.OnThrow;
import org.glowroot.agent.plugin.api.weaving.Pointcut;
public class ExecutorAspect {
private static final Logger logger = Agent.getLogger(ExecutorAspect.class);
private static final AtomicBoolean isDoneExceptionLogged = new AtomicBoolean();
// the field and method names are verbose to avoid conflict since they will become fields
// and methods in all classes that extend Runnable, Callable and/or ForkJoinTask
@Mixin({"java.lang.Runnable", "java.util.concurrent.Callable",
"java.util.concurrent.ForkJoinTask", "akka.jsr166y.ForkJoinTask",
"scala.concurrent.forkjoin.ForkJoinTask"})
public abstract static class RunnableEtcImpl implements RunnableEtcMixin {
private volatile @Nullable AuxThreadContext glowroot$auxContext;
@Override
public @Nullable AuxThreadContext glowroot$getAuxContext() {
return glowroot$auxContext;
}
@Override
public void glowroot$setAuxContext(@Nullable AuxThreadContext auxContext) {
this.glowroot$auxContext = auxContext;
}
}
@Mixin("org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor")
public static class SuppressedRunnableImpl implements SuppressedRunnableEtcMixin {}
// the method names are verbose to avoid conflict since they will become methods in all classes
// that extend Runnable, Callable and/or ForkJoinTask
public interface RunnableEtcMixin {
@Nullable
AuxThreadContext glowroot$getAuxContext();
void glowroot$setAuxContext(@Nullable AuxThreadContext auxContext);
}
// the method names are verbose to avoid conflict since they will become methods in all classes
// that extend java.lang.Runnable and/or java.util.concurrent.Callable
public interface SuppressedRunnableEtcMixin {}
@Pointcut(
className = "java.util.concurrent.Executor|java.util.concurrent.ExecutorService"
+ "|java.util.concurrent.ForkJoinPool"
+ "|org.springframework.core.task.AsyncTaskExecutor"
+ "|org.springframework.core.task.AsyncListenableTaskExecutor"
+ "|akka.jsr166y.ForkJoinPool"
+ "|scala.concurrent.forkjoin.ForkJoinPool",
methodName = "execute|submit|invoke|submitListenable",
methodParameterTypes = {".."}, nestingGroup = "executor-execute")
public static class ExecuteAdvice {
@IsEnabled
public static boolean isEnabled(@BindParameter Object runnableEtc) {
// this class may have been loaded before class file transformer was added to jvm
return runnableEtc instanceof RunnableEtcMixin
&& !(runnableEtc instanceof SuppressedRunnableEtcMixin);
}
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Object runnableEtc) {
RunnableEtcMixin runnableMixin = (RunnableEtcMixin) runnableEtc;
AuxThreadContext auxContext = context.createAuxThreadContext();
runnableMixin.glowroot$setAuxContext(auxContext);
}
}
@Pointcut(className = "com.google.common.util.concurrent.ListenableFuture",
methodName = "addListener",
methodParameterTypes = {"java.lang.Runnable", "java.util.concurrent.Executor"},
nestingGroup = "executor-add-listener")
public static class AddListenerAdvice {
@IsEnabled
public static boolean isEnabled(@BindParameter Object runnableEtc) {
return ExecuteAdvice.isEnabled(runnableEtc);
}
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Object runnableEtc) {
ExecuteAdvice.onBefore(context, runnableEtc);
}
}
@Pointcut(
className = "java.util.concurrent.ExecutorService|java.util.concurrent.ForkJoinPool"
+ "|akka.jsr166y.ForkJoinPool|scala.concurrent.forkjoin.ForkJoinPool",
methodName = "invokeAll|invokeAny",
methodParameterTypes = {"java.util.Collection", ".."},
nestingGroup = "executor-execute")
public static class InvokeAnyAllAdvice {
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Collection<?> callables) {
if (callables == null) {
return;
}
for (Object callable : callables) {
// this class may have been loaded before class file transformer was added to jvm
if (callable instanceof RunnableEtcMixin
&& !(callable instanceof SuppressedRunnableEtcMixin)) {
RunnableEtcMixin callableMixin = (RunnableEtcMixin) callable;
AuxThreadContext auxContext = context.createAuxThreadContext();
callableMixin.glowroot$setAuxContext(auxContext);
}
}
}
}
@Pointcut(className = "java.util.concurrent.ScheduledExecutorService", methodName = "schedule",
methodParameterTypes = {".."}, nestingGroup = "executor-execute")
public static class ScheduleAdvice {
@IsEnabled
public static boolean isEnabled(@BindParameter Object runnableEtc) {
// this class may have been loaded before class file transformer was added to jvm
return runnableEtc instanceof RunnableEtcMixin
&& !(runnableEtc instanceof SuppressedRunnableEtcMixin);
}
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Object runnableEtc) {
RunnableEtcMixin runnableEtcMixin = (RunnableEtcMixin) runnableEtc;
AuxThreadContext auxContext = context.createAuxThreadContext();
runnableEtcMixin.glowroot$setAuxContext(auxContext);
}
}
@Pointcut(className = "akka.actor.Scheduler", methodName = "scheduleOnce",
methodParameterTypes = {"scala.concurrent.duration.FiniteDuration",
"java.lang.Runnable", ".."},
nestingGroup = "executor-execute")
public static class ScheduleOnceAdvice {
@IsEnabled
public static boolean isEnabled(@SuppressWarnings("unused") @BindParameter Object duration,
@BindParameter Object runnableEtc) {
// this class may have been loaded before class file transformer was added to jvm
return runnableEtc instanceof RunnableEtcMixin
&& !(runnableEtc instanceof SuppressedRunnableEtcMixin);
}
@OnBefore
public static void onBefore(ThreadContext context,
@SuppressWarnings("unused") @BindParameter Object duration,
@BindParameter Object runnableEtc) {
RunnableEtcMixin runnableEtcMixin = (RunnableEtcMixin) runnableEtc;
AuxThreadContext auxContext = context.createAuxThreadContext();
runnableEtcMixin.glowroot$setAuxContext(auxContext);
}
}
@Pointcut(className = "java.util.Timer", methodName = "schedule",
methodParameterTypes = {"java.util.TimerTask", ".."}, nestingGroup = "executor-execute")
public static class TimerScheduleAdvice {
@IsEnabled
public static boolean isEnabled(@BindParameter Object runnableEtc) {
// this class may have been loaded before class file transformer was added to jvm
return runnableEtc instanceof RunnableEtcMixin
&& !(runnableEtc instanceof SuppressedRunnableEtcMixin);
}
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Object runnableEtc) {
RunnableEtcMixin runnableEtcMixin = (RunnableEtcMixin) runnableEtc;
AuxThreadContext auxContext = context.createAuxThreadContext();
runnableEtcMixin.glowroot$setAuxContext(auxContext);
}
}
// TODO revisit this
// this method uses submit() and returns Future, but none of the callers use/wait on the Future
@Pointcut(className = "net.sf.ehcache.store.disk.DiskStorageFactory", methodName = "schedule",
methodParameterTypes = {"java.util.concurrent.Callable"},
nestingGroup = "executor-execute")
public static class EhcacheDiskStorageScheduleAdvice {}
@Pointcut(className = "javax.servlet.AsyncContext", methodName = "start",
methodParameterTypes = {"java.lang.Runnable"})
public static class StartAdvice {
@IsEnabled
public static boolean isEnabled(@BindParameter Runnable runnable) {
// this class may have been loaded before class file transformer was added to jvm
return runnable instanceof RunnableEtcMixin;
}
@OnBefore
public static void onBefore(ThreadContext context, @BindParameter Object runnable) {
RunnableEtcMixin runnableMixin = (RunnableEtcMixin) runnable;
AuxThreadContext auxContext = context.createAuxThreadContext();
runnableMixin.glowroot$setAuxContext(auxContext);
}
}
@Pointcut(className = "java.util.concurrent.Future", methodName = "get",
methodParameterTypes = {".."}, timerName = "wait on future",
suppressibleUsingKey = "wait-on-future")
public static class FutureGetAdvice {
private static final TimerName timerName = Agent.getTimerName(FutureGetAdvice.class);
@IsEnabled
public static boolean isEnabled(@BindReceiver Future<?> future,
@BindClassMeta FutureClassMeta futureClassMeta) {
if (futureClassMeta.isNonStandardFuture()) {
// this is to handle known non-standard Future implementations
return false;
}
// don't capture if already done, primarily this is to avoid caching pattern where
// a future is used to store the value to ensure only-once initialization
try {
return !future.isDone();
} catch (Exception e) {
logger.debug(e.getMessage(), e);
if (!isDoneExceptionLogged.getAndSet(true)) {
logger.info("encountered a non-standard java.util.concurrent.Future"
+ " implementation, please report this stack trace to the Glowroot"
+ " project:", e);
}
return false;
}
}
@OnBefore
public static Timer onBefore(ThreadContext context) {
return context.startTimer(timerName);
}
@OnAfter
public static void onAfter(@BindTraveler Timer timer) {
timer.stop();
}
}
// the nesting group only starts applying once auxiliary thread context is started (it does not
// apply to OptionalThreadContext that miss)
@Pointcut(className = "java.lang.Runnable", methodName = "run", methodParameterTypes = {},
nestingGroup = "executor-run")
public static class RunnableAdvice {
@IsEnabled
public static boolean isEnabled(@BindReceiver Runnable runnable) {
if (!(runnable instanceof RunnableEtcMixin)) {
// this class was loaded before class file transformer was added to jvm
return false;
}
RunnableEtcMixin runnableMixin = (RunnableEtcMixin) runnable;
return runnableMixin.glowroot$getAuxContext() != null;
}
@OnBefore
public static @Nullable TraceEntry onBefore(@BindReceiver Runnable runnable) {
RunnableEtcMixin runnableMixin = (RunnableEtcMixin) runnable;
AuxThreadContext auxContext = runnableMixin.glowroot$getAuxContext();
if (auxContext == null) {
// this is unlikely (since checked in @IsEnabled) but possible under concurrency
return null;
}
runnableMixin.glowroot$setAuxContext(null);
return auxContext.start();
}
@OnReturn
public static void onReturn(@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.end();
}
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t,
@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.endWithError(t);
}
}
}
// the nesting group only starts applying once auxiliary thread context is started (it does not
// apply to OptionalThreadContext that miss)
@Pointcut(className = "java.util.concurrent.Callable", methodName = "call",
methodParameterTypes = {}, nestingGroup = "executor-run")
public static class CallableAdvice {
@IsEnabled
public static boolean isEnabled(@BindReceiver Callable<?> callable) {
if (!(callable instanceof RunnableEtcMixin)) {
// this class was loaded before class file transformer was added to jvm
return false;
}
RunnableEtcMixin callableMixin = (RunnableEtcMixin) callable;
return callableMixin.glowroot$getAuxContext() != null;
}
@OnBefore
public static @Nullable TraceEntry onBefore(@BindReceiver Callable<?> callable) {
RunnableEtcMixin callableMixin = (RunnableEtcMixin) callable;
AuxThreadContext auxContext = callableMixin.glowroot$getAuxContext();
if (auxContext == null) {
// this is unlikely (since checked in @IsEnabled) but possible under concurrency
return null;
}
callableMixin.glowroot$setAuxContext(null);
return auxContext.start();
}
@OnReturn
public static void onReturn(@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.end();
}
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t,
@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.endWithError(t);
}
}
}
// the nesting group only starts applying once auxiliary thread context is started (it does not
// apply to OptionalThreadContext that miss)
@Pointcut(className = "java.util.concurrent.ForkJoinTask|akka.jsr166y.ForkJoinTask"
+ "|scala.concurrent.forkjoin.ForkJoinTask",
methodName = "exec", methodParameterTypes = {}, nestingGroup = "executor-run")
public static class ExecAdvice {
@IsEnabled
public static boolean isEnabled(@BindReceiver Object task) {
if (!(task instanceof RunnableEtcMixin)) {
// this class was loaded before class file transformer was added to jvm
return false;
}
RunnableEtcMixin taskMixin = (RunnableEtcMixin) task;
return taskMixin.glowroot$getAuxContext() != null;
}
@OnBefore
public static @Nullable TraceEntry onBefore(@BindReceiver Object task) {
RunnableEtcMixin taskMixin = (RunnableEtcMixin) task;
AuxThreadContext auxContext = taskMixin.glowroot$getAuxContext();
if (auxContext == null) {
// this is unlikely (since checked in @IsEnabled) but possible under concurrency
return null;
}
taskMixin.glowroot$setAuxContext(null);
return auxContext.start();
}
@OnReturn
public static void onReturn(@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.end();
}
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t,
@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry != null) {
traceEntry.endWithError(t);
}
}
}
// ========== debug ==========
// KEEP THIS CODE IT IS VERY USEFUL
// private static final ThreadLocal<?> inAuxDebugLogging;
//
// static {
// try {
// Class<?> clazz = Class.forName("org.glowroot.agent.impl.AuxThreadContextImpl");
// Field field = clazz.getDeclaredField("inAuxDebugLogging");
// field.setAccessible(true);
// inAuxDebugLogging = (ThreadLocal<?>) field.get(null);
// } catch (Exception e) {
// throw new IllegalStateException(e);
// }
// }
//
// @Pointcut(className = "/(?!org.glowroot).*/", methodName = "<init>",
// methodParameterTypes = {".."})
// public static class RunnableInitAdvice {
//
// @OnAfter
// public static void onAfter(OptionalThreadContext context, @BindReceiver Object obj) {
// if (obj instanceof Runnable && isNotGlowrootThread()
// && inAuxDebugLogging.get() == null) {
// new Exception(
// "Init " + Thread.currentThread().getName() + " " + obj.getClass().getName()
// + ":" + obj.hashCode() + " " + context.getClass().getName())
// .printStackTrace();
// }
// }
// }
//
// @Pointcut(className = "java.lang.Runnable", methodName = "run", methodParameterTypes = {},
// order = 1)
// public static class RunnableRunAdvice {
//
// @IsEnabled
// public static boolean isEnabled() {
// return isNotGlowrootThread();
// }
//
// @OnBefore
// public static void onBefore(OptionalThreadContext context, @BindReceiver Runnable obj) {
// new Exception("Run " + Thread.currentThread().getName() + " " + obj.getClass().getName()
// + ":" + obj.hashCode() + " " + context.getClass().getName()).printStackTrace();
// }
// }
//
// private static boolean isNotGlowrootThread() {
// String threadName = Thread.currentThread().getName();
// return !threadName.contains("GRPC") && !threadName.contains("Glowroot");
// }
}