Skip to content

Commit

Permalink
[FIXED JENKINS-19453] Propose fix: mask interrupt while loading a class
Browse files Browse the repository at this point in the history
I think the crux of the issue is that, in general, a failed class loading
will never get retried. It is certainly the case when another class loading has triggered a
recurisve class loading as in this test case.

In #19 Jesse proposed to fix
this by dropping the whole RemoteClassLoader. But this will not work
since the said RemoteClassLoader cloud have loaded other classes that
might be already running. Dropping a classloader will not drop these
classes, and we end up just creating another classloader that loads
incompatible classes. That is a disaster waiting to happen.

In this fix, we simply make findClass non-interruptible. That is, if a
blocking remote operation gets interrupted, we catch that and simply
retry and refuse to give up. We'll remember to set the interrupt flag
back on, however, so that the interrupt signal doesn't disappear.

The net effect is as if the delivery of the interrupt was bit late than
usual. The way I see it, this is the only way to fix this.

The nesting of control structures here is horrible, but I can't think of
any better way to write this.
  • Loading branch information
kohsuke committed Nov 11, 2013
1 parent b68e3e9 commit 4253015
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 19 deletions.
63 changes: 44 additions & 19 deletions src/main/java/hudson/remoting/RemoteClassLoader.java
Expand Up @@ -214,29 +214,50 @@ ClassReference toRef(ClassFile2 cf) {
RemoteClassLoader rcl = (RemoteClassLoader) cl; RemoteClassLoader rcl = (RemoteClassLoader) cl;
synchronized (_getClassLoadingLock(rcl, name)) { synchronized (_getClassLoadingLock(rcl, name)) {
Class<?> c = rcl.findLoadedClass(name); Class<?> c = rcl.findLoadedClass(name);

boolean interrupted = false;
try { try {
if (TESTING) { // the code in this try block may throw InterruptException, but findClass
Thread.sleep(1000); // method is supposed to be uninterruptible. So we catch interrupt exception
} // and just retry until it succeeds, but in the end we set the interrupt flag
if (c!=null) return c; // back on to let the interrupt in the next earliest occasion.


// TODO: check inner class handling while (true) {
Future<byte[]> img = cr.classImage.resolve(channel, name.replace('.', '/') + ".class");
if (img.isDone()) {
try { try {
return rcl.loadClassFile(name, img.get()); if (TESTING) {
} catch (ExecutionException x) { Thread.sleep(1000);
// failure to retrieve a jar shouldn't fail the classloading }
if (c!=null) return c;

// TODO: check inner class handling
Future<byte[]> img = cr.classImage.resolve(channel, name.replace('.', '/') + ".class");
if (img.isDone()) {
try {
return rcl.loadClassFile(name, img.get());
} catch (ExecutionException x) {
// failure to retrieve a jar shouldn't fail the classloading
}
}

// if the load activity is still pending, or if the load had failed,
// fetch just this class file
return rcl.loadClassFile(name, proxy.fetch(name));
} catch (IOException x) {
throw new ClassNotFoundException(name,x);
} catch (InterruptedException x) {
// pretend as if this operation is not interruptible.
// but we need to remember to set the interrupt flag back on
// before we leave this call.
interrupted = true;
continue; // JENKINS-19453: retry
} }
}


// if the load activitiy is still pending, or if the load had failed, // no code is allowed to reach here
// fetch just this class file }
return rcl.loadClassFile(name, proxy.fetch(name)); } finally {
} catch (IOException x) { // process the interrupt later.
throw new ClassNotFoundException(name,x); if (interrupted)
} catch (InterruptedException x) { Thread.currentThread().interrupt();
throw new ClassNotFoundException(name,x);
} }
} }
} else { } else {
Expand Down Expand Up @@ -275,7 +296,11 @@ private static Object _getClassLoadingLock(RemoteClassLoader rcl, String name) {
} }
return rcl; return rcl;
} }
/** JENKINS-6604 */ /**
* Induce a large delay in {@link RemoteClassLoader#findClass(String)} to allow
*
* See JENKINS-6604
*/
static boolean TESTING; static boolean TESTING;


private Class<?> loadClassFile(String name, byte[] bytes) { private Class<?> loadClassFile(String name, byte[] bytes) {
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/hudson/remoting/ClassRemotingTest.java
Expand Up @@ -134,7 +134,11 @@ public void testInterruption() throws Exception {
} }
} }
}); });

// This is so that the first two class loads succeed but the third fails.
// A better test would use semaphores rather than timing (cf. the test before this one).
Thread.sleep(2500); Thread.sleep(2500);

f1.cancel(true); f1.cancel(true);
try { try {
Object o = f1.get(); Object o = f1.get();
Expand All @@ -145,6 +149,8 @@ public void testInterruption() throws Exception {
} catch (CancellationException x) { } catch (CancellationException x) {
// good // good
} }

// verify that classes that we tried to load aren't irrevocably damaged and it's still available
assertNotNull(channel.call(c)); assertNotNull(channel.call(c));
} finally { } finally {
RemoteClassLoader.TESTING = false; RemoteClassLoader.TESTING = false;
Expand Down
4 changes: 4 additions & 0 deletions src/test/java/hudson/remoting/DummyClassLoader.java
Expand Up @@ -37,6 +37,10 @@
* out of nowhere, to test {@link RemoteClassLoader} by creating a class * out of nowhere, to test {@link RemoteClassLoader} by creating a class
* that only exists on one side of the channel but not the other. * that only exists on one side of the channel but not the other.
* *
* <p>
* Given a class in a "remoting" package, this classloader is capable of loading the same version of the class
* in the "rem0ting" package.
*
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
class DummyClassLoader extends ClassLoader { class DummyClassLoader extends ClassLoader {
Expand Down
3 changes: 3 additions & 0 deletions src/test/java/hudson/remoting/TestLinkage.java
Expand Up @@ -27,6 +27,9 @@
public class TestLinkage implements Callable { public class TestLinkage implements Callable {


public Object call() throws Throwable { public Object call() throws Throwable {
// force classloading of several classes
// when we run this test, we insert an artificial delay to each classloading
// so that we can intercept the classloading.
return A.field; return A.field;
} }


Expand Down

0 comments on commit 4253015

Please sign in to comment.