Skip to content

Commit

Permalink
Merge pull request quarkusio#41692 from gsmet/diag-cl-closed
Browse files Browse the repository at this point in the history
Provide infrastructure to debug QuarkusClassLoader lifecyle
  • Loading branch information
gsmet authored Jul 5, 2024
2 parents bf60876 + 6272732 commit 1c1b7ef
Showing 1 changed file with 65 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
*/
public class QuarkusClassLoader extends ClassLoader implements Closeable {
private static final Logger log = Logger.getLogger(QuarkusClassLoader.class);
private static final Logger lifecycleLog = Logger.getLogger(QuarkusClassLoader.class.getName() + ".lifecycle");
private static final boolean LOG_ACCESS_TO_CLOSED_CLASS_LOADERS = Boolean
.getBoolean("quarkus-log-access-to-closed-class-loaders");

private static final byte STATUS_OPEN = 1;
private static final byte STATUS_CLOSING = 0;
private static final byte STATUS_CLOSED = -1;

protected static final String META_INF_SERVICES = "META-INF/services/";
protected static final String JAVA = "java.";

Expand Down Expand Up @@ -103,7 +111,7 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) {
private final List<ClassLoaderEventListener> classLoaderEventListeners;

/**
* The element that holds resettable in-memory classses.
* The element that holds resettable in-memory classes.
* <p>
* A reset occurs when new transformers and in-memory classes are added to a ClassLoader. It happens after each
* start in dev mode, however in general the reset resources will be the same. There are some cases where this is
Expand Down Expand Up @@ -131,14 +139,15 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) {
PLATFORM_CLASS_LOADER = cl;
}

private boolean closed;
private volatile byte status;
private volatile boolean driverLoaded;

private QuarkusClassLoader(Builder builder) {
// Not passing the name to the parent constructor on purpose:
// stacktraces become very ugly if we do that.
super(builder.parent);
this.name = builder.name;
this.status = STATUS_OPEN;
this.elements = builder.elements;
this.bannedElements = builder.bannedElements;
this.parentFirstElements = builder.parentFirstElements;
Expand All @@ -151,6 +160,10 @@ private QuarkusClassLoader(Builder builder) {
this.classLoaderEventListeners = builder.classLoaderEventListeners.isEmpty() ? Collections.emptyList()
: builder.classLoaderEventListeners;
setDefaultAssertionStatus(builder.assertionsEnabled);

if (lifecycleLog.isDebugEnabled()) {
lifecycleLog.debugf(new RuntimeException("Created to log a stacktrace"), "Creating class loader %s", this);
}
}

public static Builder builder(String name, ClassLoader parent, boolean parentFirst) {
Expand All @@ -171,6 +184,8 @@ private String sanitizeName(String name) {
* Returns true if the supplied class is a class that would be loaded parent-first
*/
public boolean isParentFirst(String name) {
ensureOpen();

if (name.startsWith(JAVA)) {
return true;
}
Expand Down Expand Up @@ -198,6 +213,8 @@ private boolean parentFirst(String name, ClassLoaderState state) {
}

public void reset(Map<String, byte[]> generatedResources, Map<String, byte[]> transformedClasses) {
ensureOpen();

if (resettableElement == null) {
throw new IllegalStateException("Classloader is not resettable");
}
Expand All @@ -210,10 +227,14 @@ public void reset(Map<String, byte[]> generatedResources, Map<String, byte[]> tr

@Override
public Enumeration<URL> getResources(String unsanitisedName) throws IOException {
ensureOpen();

return getResources(unsanitisedName, false);
}

public Enumeration<URL> getResources(String unsanitisedName, boolean parentAlreadyFoundResources) throws IOException {
ensureOpen();

for (ClassLoaderEventListener l : classLoaderEventListeners) {
l.enumeratingResourceURLs(unsanitisedName, this.name);
}
Expand Down Expand Up @@ -244,7 +265,7 @@ public Enumeration<URL> getResources(String unsanitisedName, boolean parentAlrea
}
}
//TODO: in theory resources could have been added in dev mode
//but I don't thing this really matters for this code path
//but I don't think this really matters for this code path
Set<URL> resources = new LinkedHashSet<>();
ClassPathElement[] providers = state.loadableResources.get(name);
if (providers != null) {
Expand Down Expand Up @@ -356,6 +377,8 @@ private ClassLoaderState getState() {

@Override
public URL getResource(String unsanitisedName) {
ensureOpen();

for (ClassLoaderEventListener l : classLoaderEventListeners) {
l.gettingURLFromResource(unsanitisedName, this.name);
}
Expand Down Expand Up @@ -404,6 +427,8 @@ public URL getResource(String unsanitisedName) {

@Override
public InputStream getResourceAsStream(String unsanitisedName) {
ensureOpen();

for (ClassLoaderEventListener l : classLoaderEventListeners) {
l.openResourceStream(unsanitisedName, this.name);
}
Expand Down Expand Up @@ -455,6 +480,8 @@ public InputStream getResourceAsStream(String unsanitisedName) {
*/
@Override
protected Class<?> findClass(String moduleName, String name) {
ensureOpen();

try {
return loadClass(name, false);
} catch (ClassNotFoundException e) {
Expand All @@ -463,21 +490,29 @@ protected Class<?> findClass(String moduleName, String name) {
}

protected URL findResource(String name) {
ensureOpen();

return getResource(name);
}

@Override
protected Enumeration<URL> findResources(String name) throws IOException {
ensureOpen();

return getResources(name);
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
ensureOpen();

return loadClass(name, false);
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
ensureOpen();

for (ClassLoaderEventListener l : classLoaderEventListeners) {
l.loadClass(name, this.name);
}
Expand Down Expand Up @@ -574,10 +609,14 @@ private String getPackageNameFromClassName(String className) {
}

public List<ClassPathElement> getElementsWithResource(String name) {
ensureOpen();

return getElementsWithResource(name, false);
}

public List<ClassPathElement> getElementsWithResource(String name, boolean localOnly) {
ensureOpen();

List<ClassPathElement> ret = new ArrayList<>();
if (parent instanceof QuarkusClassLoader && !localOnly) {
ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name));
Expand All @@ -591,6 +630,8 @@ public List<ClassPathElement> getElementsWithResource(String name, boolean local
}

public List<String> getLocalClassNames() {
ensureOpen();

List<String> ret = new ArrayList<>();
for (String name : getState().loadableResources.keySet()) {
if (name.endsWith(".class")) {
Expand All @@ -602,10 +643,14 @@ public List<String> getLocalClassNames() {
}

public Class<?> visibleDefineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
ensureOpen();

return super.defineClass(name, b, off, len);
}

public void addCloseTask(Runnable task) {
ensureOpen();

synchronized (closeTasks) {
closeTasks.add(task);
}
Expand All @@ -614,11 +659,16 @@ public void addCloseTask(Runnable task) {
@Override
public void close() {
synchronized (this) {
if (closed) {
if (status < STATUS_OPEN) {
return;
}
closed = true;
status = STATUS_CLOSING;
}

if (lifecycleLog.isDebugEnabled()) {
lifecycleLog.debugf(new RuntimeException("Created to log a stacktrace"), "Closing class loader %s", this);
}

List<Runnable> tasks;
synchronized (closeTasks) {
tasks = new ArrayList<>(closeTasks);
Expand Down Expand Up @@ -664,10 +714,19 @@ public void close() {
}
ResourceBundle.clearCache(this);

status = STATUS_CLOSED;
}

public boolean isClosed() {
return closed;
return status < STATUS_OPEN;
}

private void ensureOpen() {
if (LOG_ACCESS_TO_CLOSED_CLASS_LOADERS && status == STATUS_CLOSED) {
// we do not use a logger as it might require some class loading
System.out.println("Class loader " + this + " has been closed and may not be accessed anymore");
Thread.dumpStack();
}
}

@Override
Expand Down

0 comments on commit 1c1b7ef

Please sign in to comment.