Skip to content

Commit ed54dc8

Browse files
authored
Avoid blocking in ASMBasedTaskDescriptor (linkedin#336)
Currently the ASMBasedTaskDescriptor uses ByteBuddy to hook into the class loader to analyze bytecode so that ParSeq can generate descriptions of tasks that were created via lambda functions to aid in debugging and tracing. In production, blocking on the lambda can cause a global lockout of threads. This can happen because of the common fork join pool being starved or because the classes being analyzed take a while. If a class has already been analyzed, we do not need to await the latch because we have a thread safe collection to access. Classes can be analyzed multiple times in a JVM lifecycle because of class unloading, but we don't care about subsequent analyses, (the bytecode should not change with load -> unload -> load).
1 parent 51ce337 commit ed54dc8

File tree

3 files changed

+98
-100
lines changed

3 files changed

+98
-100
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
v5.1.14
2+
------
3+
* Update ASMBasedTaskDescriptor to avoid blocking when classes have already been analyzed
4+
15
v5.1.13
26
------
37
* Pin python version for the "publish" github action

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=5.1.13
1+
version=5.1.14
22
group=com.linkedin.parseq
33
org.gradle.parallel=true

subprojects/parseq-lambda-names/src/main/java/com/linkedin/parseq/lambda/ASMBasedTaskDescriptor.java

Lines changed: 93 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import net.bytebuddy.matcher.ElementMatchers;
3434
import net.bytebuddy.utility.JavaModule;
3535

36+
3637
/**
3738
* An ASM based implementation of {@link TaskDescriptor} to provide description for generated Lambda class.
3839
* Description of Lambda expression includes source code location of lambda, function call or method reference
@@ -71,112 +72,109 @@ static void onExit(@Advice.Argument(0) Class<?> hostClass, @Advice.Argument(1) b
7172

7273
static {
7374

74-
try {
75-
Instrumentation inst = ByteBuddyAgent.install();
76-
77-
/*
78-
* If we can get the instance of jdk.internal.misc.Unsafe then we will
79-
* attempt to instrument Unsafe.defineAnonymousClass(...) to capture classes
80-
* generated for lambdas.
81-
* This approach does not work for Oracle Java 8 because
82-
* sun.misc.Unsafe.defineAnonymousClass(...) is a native method and we can
83-
* at most replace it but there is no reasonably easy way to replace it and
84-
* still invoke the original method.
85-
*/
86-
boolean isJdkUnsafe = false;
75+
try {
76+
Instrumentation inst = ByteBuddyAgent.install();
77+
78+
/*
79+
* If we can get the instance of jdk.internal.misc.Unsafe then we will
80+
* attempt to instrument Unsafe.defineAnonymousClass(...) to capture classes
81+
* generated for lambdas.
82+
* This approach does not work for Oracle Java 8 because
83+
* sun.misc.Unsafe.defineAnonymousClass(...) is a native method and we can
84+
* at most replace it but there is no reasonably easy way to replace it and
85+
* still invoke the original method.
86+
*/
87+
boolean isJdkUnsafe = false;
8788
Class<?> unsafe = null;
8889
try {
8990
unsafe = Class.forName("jdk.internal.misc.Unsafe");
9091
isJdkUnsafe = true;
9192
} catch (ClassNotFoundException e) {
9293
}
9394

94-
if (isJdkUnsafe) {
95-
// Code path that supports OpenJDK Java 11 and up
96-
97-
/*
98-
* Inject AnalyzerAdvice to boot ClassLoader.
99-
* It has to be reachable from jdk.internal.misc.Unsafe.
100-
*/
101-
ClassInjector.UsingUnsafe.ofBootLoader()
102-
.inject(Collections.singletonMap(
103-
new TypeDescription.ForLoadedType(AnalyzerAdvice.class),
104-
ClassFileLocator.ForClassLoader.read(AnalyzerAdvice.class)
105-
)
106-
);
107-
108-
/*
109-
* Inject the analyze(byte[] byteCode, ClassLoader loader) method from this ClassLoader
110-
* to the AnalyzerAdvice class from boot ClassLoader.
111-
*/
112-
Class<?> injectedInt = ClassLoader.getSystemClassLoader().getParent().loadClass(AnalyzerAdvice.class.getName());
113-
injectedInt.getField("_method").set(null, Analyzer.class.getDeclaredMethod("analyze", byte[].class, ClassLoader.class));
114-
115-
JavaModule module = JavaModule.ofType(injectedInt);
116-
117-
new AgentBuilder.Default()
118-
.disableClassFormatChanges()
119-
.ignore(noneOf(unsafe))
120-
.with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
121-
.with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
122-
.with(AgentBuilder.TypeStrategy.Default.REDEFINE)
123-
.with(AgentBuilder.InjectionStrategy.UsingUnsafe.INSTANCE)
124-
.assureReadEdgeTo(inst, module)
125-
.type(is(unsafe))
126-
.transform(new AgentBuilder.Transformer() {
127-
@Override
128-
public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription,
129-
ClassLoader classLoader, JavaModule module) {
130-
return builder.visit(Advice.to(AnalyzerAdvice.class).on(ElementMatchers.named("defineAnonymousClass")));
131-
}
132-
}).installOnByteBuddyAgent();
133-
} else {
134-
// Code path that supports Oracle Java 8 and 9
95+
if (isJdkUnsafe) {
96+
// Code path that supports OpenJDK Java 11 and up
97+
98+
/*
99+
* Inject AnalyzerAdvice to boot ClassLoader.
100+
* It has to be reachable from jdk.internal.misc.Unsafe.
101+
*/
102+
ClassInjector.UsingUnsafe.ofBootLoader()
103+
.inject(Collections.singletonMap(new TypeDescription.ForLoadedType(AnalyzerAdvice.class),
104+
ClassFileLocator.ForClassLoader.read(AnalyzerAdvice.class)));
105+
106+
/*
107+
* Inject the analyze(byte[] byteCode, ClassLoader loader) method from this ClassLoader
108+
* to the AnalyzerAdvice class from boot ClassLoader.
109+
*/
110+
Class<?> injectedInt = ClassLoader.getSystemClassLoader().getParent().loadClass(AnalyzerAdvice.class.getName());
111+
injectedInt.getField("_method")
112+
.set(null, Analyzer.class.getDeclaredMethod("analyze", byte[].class, ClassLoader.class));
113+
114+
JavaModule module = JavaModule.ofType(injectedInt);
115+
116+
new AgentBuilder.Default().disableClassFormatChanges()
117+
.ignore(noneOf(unsafe))
118+
.with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
119+
.with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
120+
.with(AgentBuilder.TypeStrategy.Default.REDEFINE)
121+
.with(AgentBuilder.InjectionStrategy.UsingUnsafe.INSTANCE)
122+
.assureReadEdgeTo(inst, module)
123+
.type(is(unsafe))
124+
.transform(new AgentBuilder.Transformer() {
125+
@Override
126+
public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,
127+
JavaModule module) {
128+
return builder.visit(Advice.to(AnalyzerAdvice.class).on(ElementMatchers.named("defineAnonymousClass")));
129+
}
130+
})
131+
.installOnByteBuddyAgent();
132+
} else {
133+
// Code path that supports Oracle Java 8 and 9
135134
inst.addTransformer(new Analyzer());
136-
}
137-
} catch(Exception e) {
138-
e.printStackTrace();
139-
}
135+
}
136+
} catch (Exception e) {
137+
e.printStackTrace();
138+
}
140139
}
141140

142141
@Override
143142
public String getDescription(String className) {
144143
Optional<String> lambdaClassDescription = getLambdaClassDescription(className);
145-
if (lambdaClassDescription.isPresent()) {
146-
return lambdaClassDescription.get();
147-
} else {
148-
return className;
149-
}
144+
return lambdaClassDescription.orElse(className);
150145
}
151146

152147
Optional<String> getLambdaClassDescription(String className) {
148+
int slashIndex = className.lastIndexOf('/');
149+
// If we can't find the slash, we can't find the name of the lambda.
150+
if (slashIndex <= 0) {
151+
return Optional.empty();
152+
}
153+
String name = className.substring(0, slashIndex);
154+
String description = _names.get(name);
155+
156+
// If we have already analyzed the class, we don't need to await
157+
// analysis on other lambdas.
158+
if (description != null) {
159+
return Optional.of(description).filter(s -> !s.isEmpty());
160+
}
161+
153162
CountDownLatch latch = _latchRef.get();
154163
if (latch != null) {
155164
try {
156-
/*
157-
* We wait up to one minute - an arbitrary, sufficiently large amount of time.
158-
* The wait period must be bounded to avoid locking out JVM.
159-
*/
165+
// We wait up to one minute - an arbitrary, sufficiently large amount of time.
166+
// The wait period must be bounded to avoid locking out JVM.
160167
latch.await(1, TimeUnit.MINUTES);
161168
} catch (InterruptedException e) {
162-
System.out.println("ERROR: ParSeq Latch timed out suggesting serious issue in ASMBasedTaskDescriptor. "
163-
+ "Current number of class being analyzed: " + String.valueOf(_count.get()));
169+
System.err.println("ERROR: ParSeq Latch timed out suggesting serious issue in ASMBasedTaskDescriptor. "
170+
+ "Current number of class being analyzed: " + _count.get());
164171
e.printStackTrace();
165172
Thread.currentThread().interrupt();
166173
}
167174
}
168-
int slashIndex = className.lastIndexOf('/');
169-
if (slashIndex > 0) {
170-
String name = className.substring(0, slashIndex);
171-
String desc = _names.get(name);
172-
if (desc == null || desc.isEmpty()) {
173-
return Optional.empty();
174-
} else {
175-
return Optional.of(desc);
176-
}
177-
}
178175

179-
return Optional.empty();
176+
// Try again
177+
return Optional.ofNullable(_names.get(name)).filter(s -> !s.isEmpty());
180178
}
181179

182180
private static void add(String lambdaClassName, String description) {
@@ -187,8 +185,7 @@ public static class Analyzer implements ClassFileTransformer {
187185

188186
@Override
189187
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
190-
ProtectionDomain protectionDomain, byte[] classfileBuffer)
191-
throws IllegalClassFormatException {
188+
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
192189
if (className == null && loader != null) {
193190
analyze(classfileBuffer, loader);
194191
}
@@ -208,23 +205,20 @@ public static void analyze(byte[] byteCode, ClassLoader loader) {
208205
}
209206
}
210207
final Exception e = new Exception();
211-
ForkJoinPool.commonPool().execute(new Runnable() {
212-
@Override
213-
public void run() {
214-
try {
215-
doAnalyze(byteCode, loader, e);
216-
} catch (Throwable t) {
217-
/*
218-
* We need to catch everything because other
219-
* threads may be blocked on CountDownLatch.
220-
*/
221-
System.out.println("WARNING: Parseq cannot doAnalyze");
222-
t.printStackTrace();
223-
}
224-
if (_count.decrementAndGet() == 0) {
225-
CountDownLatch latch = _latchRef.getAndSet(null);
226-
latch.countDown();
227-
}
208+
ForkJoinPool.commonPool().execute(() -> {
209+
try {
210+
doAnalyze(byteCode, loader, e);
211+
} catch (Throwable t) {
212+
/*
213+
* We need to catch everything because other
214+
* threads may be blocked on CountDownLatch.
215+
*/
216+
System.out.println("WARNING: Parseq cannot doAnalyze");
217+
t.printStackTrace();
218+
}
219+
if (_count.decrementAndGet() == 0) {
220+
CountDownLatch latch = _latchRef.getAndSet(null);
221+
latch.countDown();
228222
}
229223
});
230224
}

0 commit comments

Comments
 (0)