Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
I tried to write a test case to verify the hypothesis along the line of
https://issues.jenkins-ci.org/browse/JENKINS-20707?focusedCommentId=198755&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-198755,
but ultimately it was a dead-end.

The reason I was wrong is because ImportedClassLoaderTable maintain strong
references to all the imported classloaders, and it prevents them from
getting garbage collected. Probably for the reasons I was mentioning
in JENKINS-20707, RemoteClassLoaders never get garbage collected, so they should
never get unexported. I've spent much time trying to make the test case
work, so I'm going to commit it on the side just in case someone finds
useful in the future.
  • Loading branch information
kohsuke committed Apr 15, 2014
1 parent 0ccbbc6 commit bd8e65c
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 10 deletions.
100 changes: 100 additions & 0 deletions src/test/java/hudson/remoting/Bug20707Test.java
@@ -0,0 +1,100 @@
package hudson.remoting;

import hudson.remoting.util.OneShotEvent;
import org.jvnet.hudson.test.Bug;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @author Kohsuke Kawaguchi
*/
public class Bug20707Test extends RmiTestBase implements Serializable {
@Bug(20707)
public void testGc() throws Exception {
final DummyClassLoader cl = new DummyClassLoader(TestEcho.class);
final IEcho c = cl.load(TestEcho.class);
c.set(new Object[]{
new HangInducer(),
cl.load(TestEcho.class) // <- DA BOMB. We are trying to blow up the deserialization of this
});

// when the response comes back, make it hang
HANG = new OneShotEvent();

ExecutorService es = Executors.newCachedThreadPool();

final java.util.concurrent.Future<Object> f = es.submit(new java.util.concurrent.Callable<Object>() {
public Object call() throws Exception {
// send a couple of objects, which exports DummyClassLoader.
// when the computation is done on the other side, RemoteClassLoader can be garbage collected any time
// on this side, the obtained UserResponse gets passed to the thread that made the request
// (this thread)

channel.call(new TestEcho(c));

// we'll use HangInducer.HANG to make the response unmarshalling hang

return null;
}
});

// wait until the echo call comes back and hangs at the unmarshalling
BLOCKING.block();

// induce GC on the other side until we get classloader unexported
channel.call(new Callable<Void,InterruptedException>() {
public Void call() throws InterruptedException {
while (ECHO_CLASSLOADER.get()!=null) {
System.gc();
Thread.sleep(100);
}
return null;
}
});

// by the time the above Callable comes back, Unexport command has executed and classloader is unexported
assertFalse(channel.exportedObjects.isExported(cl));

// and now if we let the unmarshalling go, it'll finish deserializing HangInducer
// and will try to unmarshal DA BOMB, and it should blow up
f.get();
}

private Object writeReplace() {
return null;
}

public static interface IEcho extends Callable<Object,IOException> {
void set(Object o);
Object get();
}

public static class HangInducer implements Serializable {
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
if (Channel.current().getName().equals("north")) {
try {
BLOCKING.signal(); // let the world know that we are hanging now
HANG.block();
} catch (InterruptedException e) {
throw new Error(e);
}
}
}

private static final long serialVersionUID = 1L;
}

private static OneShotEvent BLOCKING = new OneShotEvent();
private static OneShotEvent HANG;

static WeakReference<ClassLoader> ECHO_CLASSLOADER;

public static void set(WeakReference<ClassLoader> cl) {
ECHO_CLASSLOADER = cl;
}
}
10 changes: 5 additions & 5 deletions src/test/java/hudson/remoting/ClassRemotingTest.java
Expand Up @@ -45,7 +45,7 @@ public class ClassRemotingTest extends RmiTestBase {
public void test1() throws Throwable {
// call a class that's only available on DummyClassLoader, so that on the remote channel
// it will be fetched from this class loader and not from the system classloader.
Callable c = (Callable) DummyClassLoader.apply(TestCallable.class);
Callable c = DummyClassLoader.apply(TestCallable.class);

Object[] r = (Object[]) channel.call(c);

Expand Down Expand Up @@ -74,7 +74,7 @@ public void testRemoteProperty() throws Exception {
return;

DummyClassLoader cl = new DummyClassLoader(TestCallable.class);
Callable c = (Callable) cl.load(TestCallable.class);
Callable c = cl.load(TestCallable.class);
assertSame(c.getClass().getClassLoader(), cl);

channel.setProperty("test",c);
Expand All @@ -86,11 +86,11 @@ public void testRemoteProperty() throws Exception {
public void testRaceCondition() throws Throwable {
DummyClassLoader parent = new DummyClassLoader(TestCallable.class);
DummyClassLoader child1 = new DummyClassLoader(parent, TestCallable.Sub.class);
final Callable<Object,Exception> c1 = (Callable) child1.load(TestCallable.Sub.class);
final Callable<Object,Exception> c1 = child1.load(TestCallable.Sub.class);
assertEquals(child1, c1.getClass().getClassLoader());
assertEquals(parent, c1.getClass().getSuperclass().getClassLoader());
DummyClassLoader child2 = new DummyClassLoader(parent, TestCallable.Sub.class);
final Callable<Object,Exception> c2 = (Callable) child2.load(TestCallable.Sub.class);
final Callable<Object,Exception> c2 = child2.load(TestCallable.Sub.class);
assertEquals(child2, c2.getClass().getClassLoader());
assertEquals(parent, c2.getClass().getSuperclass().getClassLoader());
ExecutorService svc = Executors.newFixedThreadPool(2);
Expand Down Expand Up @@ -118,7 +118,7 @@ public void testInterruption() throws Exception {
DummyClassLoader parent = new DummyClassLoader(TestLinkage.B.class);
final DummyClassLoader child1 = new DummyClassLoader(parent, TestLinkage.A.class);
final DummyClassLoader child2 = new DummyClassLoader(child1, TestLinkage.class);
final Callable<Object, Exception> c = (Callable) child2.load(TestLinkage.class);
final Callable<Object, Exception> c = child2.load(TestLinkage.class);
assertEquals(child2, c.getClass().getClassLoader());
RemoteClassLoader.TESTING = true;
try {
Expand Down
8 changes: 4 additions & 4 deletions src/test/java/hudson/remoting/DummyClassLoader.java
Expand Up @@ -58,7 +58,7 @@ class Entry {
Entry(Class<?> c) {
this.c = c;
physicalName = c.getName();
assert physicalName.contains("remoting.Test");
assert physicalName.contains("remoting.Test") : physicalName;
logicalName = physicalName.replace("remoting", "rem0ting");
physicalPath = physicalName.replace('.', '/') + ".class";
logicalPath = logicalName.replace('.', '/') + ".class";
Expand Down Expand Up @@ -95,19 +95,19 @@ public DummyClassLoader(ClassLoader parent, Class<?>... classes) {
/**
* Short cut to create an instance of a transformed class.
*/
public static Object apply(Class<?> c) {
public static <T> T apply(Class<?> c) {
return new DummyClassLoader(c).load(c);
}

/**
* Loads a class that looks like an exact clone of the named class under
* a different class name.
*/
public Object load(Class c) {
public <T> T load(Class c) {
for (Entry e : entries) {
if (e.c==c) {
try {
return loadClass(e.logicalName).newInstance();
return (T)loadClass(e.logicalName).newInstance();
} catch (InstantiationException x) {
throw new Error(x);
} catch (IllegalAccessException x) {
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/hudson/remoting/DummyClassLoaderTest.java
Expand Up @@ -30,7 +30,7 @@
*/
public class DummyClassLoaderTest extends TestCase {
public void testLoad() throws Throwable {
Callable c = (Callable) DummyClassLoader.apply(TestCallable.class);
Callable c = DummyClassLoader.apply(TestCallable.class);
System.out.println(c.call());
// make sure that the returned class is loaded from the dummy classloader
assertTrue(((Object[])c.call())[0].toString().startsWith(DummyClassLoader.class.getName()));
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/hudson/remoting/TestEcho.java
@@ -0,0 +1,38 @@
package hudson.remoting;

import hudson.remoting.Bug20707Test.IEcho;

import java.io.IOException;
import java.lang.ref.WeakReference;

/**
* @author Kohsuke Kawaguchi
*/
public class TestEcho implements IEcho {
/**
* For adding arbitrary objects into the echo back.
*/
private Object o;

public TestEcho(Object o) {
this.o = o;
}

public TestEcho() {
}

public Object call() throws IOException {
Bug20707Test.set(new WeakReference<ClassLoader>(getClass().getClassLoader()));
return this;
}

public void set(Object o) {
this.o = o;
}

public Object get() {
return o;
}

private static final long serialVersionUID = 1L;
}
61 changes: 61 additions & 0 deletions src/test/java/hudson/remoting/util/OneShotEvent.java
@@ -0,0 +1,61 @@
package hudson.remoting.util;

public final class OneShotEvent {
private boolean signaled;
private final Object lock;

public OneShotEvent() {
this.lock = this;
}

public OneShotEvent(Object lock) {
this.lock = lock;
}

/**
* Non-blocking method that signals this event.
*/
public void signal() {
synchronized (lock) {
if(signaled) return;
this.signaled = true;
lock.notifyAll();
}
}

/**
* Blocks until the event becomes the signaled state.
*
* <p>
* This method blocks infinitely until a value is offered.
*/
public void block() throws InterruptedException {
synchronized (lock) {
while(!signaled)
lock.wait();
}
}

/**
* Blocks until the event becomes the signaled state.
*
* <p>
* If the specified amount of time elapses,
* this method returns null even if the value isn't offered.
*/
public void block(long timeout) throws InterruptedException {
synchronized (lock) {
if(!signaled)
lock.wait(timeout);
}
}

/**
* Returns true if a value is offered.
*/
public boolean isSignaled() {
synchronized (lock) {
return signaled;
}
}
}

0 comments on commit bd8e65c

Please sign in to comment.