Wednesday, July 18, 2007

ContextFinder in Eclipse is broken

[Updated (19 July 2007): Further discussion with Glyn Normington and Tom Watson indicates it is the Class.forName(...,TCCL) form of loading a class from the TCCL that presents the issues since it triggers the class loading constraints while the ClassLoader.loadClass form does not.]

I was doing some reading and thinking about the ThreadContextClassLoader (TCCL) issue in OSGi environments. Many libraries use the TCCL to load classes. The real problem comes when the library ONLY uses Class.forName(...,TCCL) to load a class and doesn't try it's own class loader first or TCCL.loadClass.

Eclipse created the ContextFinder to try and address the TCCL issue in OSGi. It's goal is to find a bundle's class loader on the call stack and then delegate to that class loader to handle the load request. The normal OSGi bundle class loader rules as well as a further Buddy Policy extension is then used to find the requested class.

However, it turns out there are problems with this approach when Class.forName(...,TCCL) is used. One of which is class loader constraint violation. The other is inadvertent pinning of classes in memory as they are added to the constraint table. After reading a classloader paper (from 1998!), I think the ContextFinder model (that is a single, framework-wide shared TCCL) used by Eclipse is fatally flawed. It is virtually guaranteed to violate class loading constraints in the face of multiple package versions. Not to mention the fact that it can pin classes and class loaders in memory for as long as the ContextFinder is reachable.

Specifying a way to handle TCCL in OSGi may not be possible. Given 3 bundles: A, B, C having two versions of each: A1, A2, B1, B2, C1, C2. Imagine C1 imports a package from B1 and B1 imports a package from A1. Further C2 imports a package from B2 and B2 imports a package from A2. A type from A1 does not appear in the signature of B1 and a type from A2 does not appear in the signature of B2. Thus via B1, C1 cannot "see" A1 and via B2, C2 cannot "see" A2. Also imagine that C1 import the package from A2 used by B2 and C2 imports the package from A1 used by B1. We now have the wiring depicted below:

C1 C2^^ ^^| \ / || \ / |B1 \/ B2^ /\ ^| / \ || / \ |A1 A2

This is entirely possible and works fine in normal (non-TCCL) class loading. C1 is not exposed to A1 via B1, so it's use of the package from A2 results in no conflict. If C1 loads a class from B1 which results in the load of a class from A1, then B1 is the initiating class loader of the request to A1. When C1 loads a class from A2, C1 is the initiating class loader. So C1 is never the initiating class loader for loads from both A1 and A2.

However, when we have a single, shared TCCL and bundle D calls Class.forName(...,TCCL) to load a class, the shared TCCL will be the initiating class loader for loads from both A1 and A2 (or B1 and B2, or C1 and C2) depending upon whatever context is used to select the defining class loader. C1 or C2 may have caused D to request the class load. Since the TCCL is the initiating class loader, any class load initiated by it for some class P must always return the same class object. In the presence of multiple versions (and even without multiple versions if two bundles are unlucky enough to choose the same fully qualified class name), loader constraints will eventually be violated.

Thus the Eclipse ContextFinder model is broken and we obviously should not spec it in OSGi. However, I am currently at a loss for a reliable solution to the TCCL problem for OSGi. We can certainly recommend TCCL.loadClass is used instead of Class.forName(...,TCCL) but there is a large body of code already out there which already uses the latter form.

Not necessarily. ContextFinder will delegate the class load request to a bundle's class loader found on the call stack. If the desired class is reachable from that class loader, then you are done. You can use buddy loading to extend the set of classes reachable from the bundle's class loader beyond normal import-package, require-bundle by also searching the bundles associated via the declared buddy policy.