Tuesday, October 11, 2011

Bundle dependencies resolution

Bundle dependencies resolution is the automation of dependency management. It precedes the Resolved status of a bundle. In other words, it's the process of matching a bundle's imported packages to exported packages from another bundle, so that a bundle only has access to a single version of any type. It uses classloader delegation between bundles. Failure to resolve a bundle's dependencies leaves the bundle in the Installed status.

1. Classloader delegation
The classloader delegation is the process of delegating class loading to different classloader, dependending on the package. The classloading delegation is ruled by the following algorithm :
  1. if package starts with java., ask parent classloader;
  2. if package is imported, ask the exporting bundle's classloader;
  3. search in Bundle-ClassPath.
2. Dependencies resolution

The resolution of a bundle's dependencies follow this algorithm :

resolve(bundle)
 bundle has no  imports
    Resolution is successful;
 bundle has imports :
    find a matching candidate for the imported package :
       if the candidate bundle is Resolved : 
          Resolution is successful;
       if the candidate bundle is not Resolved :
          resolve(candidate)
 wire bundles

2.1. Attributes and matching
Exported attributes (arbitrary name/value pairs) are important for matching only if the importing bundle specifies the same attribute. Otherwise, the framework just ignores them.
Imported packages with attributes specified must find a matching exported package with the same attribute and the same value. Otherwise, resolution fails.
If multiple attributes are declared, a logical and is used to resolve the dependency with attributes.


2.2. Multiple exporting bundles and version choosing
Dependency resolution may become tricky when it comes to packages exported  either with differents versions (by different bundles), or with the same version by multiple bundles. The OSGi framework uses the following algorithm to resolve such packages :

resolve(package)
 package is exported with different versions
    framework chooses the highest matching version;
 package is exported with the same version by multiple bundles :
    - priority is given to bundles installed first
    - priority then goes to maximizing collaboration
       already resolved candidates bundle have priority against not resolved ones,


"Maximizing collaboration" rule, giving priority to already resolved bundles over not resolved ones, may break the "highest version" rule. This is done because bundles can only collaborate if they are using the same version of a shared package. Furthermore, it minimizes the number of different packages versions.


So, this algorithm can be summarized like this :
 1. highest priority : already resolved candidates
    1.1. multiple matches are sorted against : 
       1.1.1. version (highest first) 
       1.1.2. installation order (lower first) 
 2. then for unresolved candidates, matches against : 
    2.1. version (highest first)  
    2.2. installation order (lower first)

3. The Uses constraint
Please note that this chapter is based on a Neil Bartlett's blog post rewritten and extended by me : Neil Bartlett's post.
Before explaining the uses constraint's goal, let's introduce the concept of bundle "class space". The bundle class space is the union of imported packages and bundle classpath. In some situations, dependencies resolution can lead to class spaces inconsistencies.


Let's illustrate this. First, we set up a simple situation. Bundle A export a package foo with version 1.0.0. A bundle B imports the package foo, witg the same version. The notation used in the schema below is taken from OSGi specifications : a black rectangle is an export, a white rectangle is an import, and the yellow "blob" container is a bundle. The line connecting an export to an import means that the OSGi framework has chosen this export to resolve the import.

Everything's fine. Now let's imagine bundle B exports a package bar. We now add a bundle C, which imports the package bar. This leads us to this situation :

Once again, no problem here. But we are going to complicate the situation by adding a bundle D, exporting the package foo (same as A's package), but with version 2.0. And then, we add in bundle C an import of package foo in version 2.0. The following schema describe the situation.

Still no problem here. But this is because package bar does not expose package foo, either by subclassing a class from package foo or by using a class of package foo as a method return type or as a method parameter. In this case, bundle class space C is consistent, which means it only has one version of each class. Bundle C's class space is shown in the following schema by the shaded blue area.

But what happens if package bar exposes package foo, by (as an example) subclassing a class from package foo, like in the following code snipet ?


In this situation, the framework is still able to resolve all dependencies, but a ClassCastException will be thrown, because the Foo class used in bundle B is located in B's class space (ie. coming from foo;1.0 import), but the Foo class in bundle C is located in C's class space. Since each OSGi bundle has its own class loader and a class is uniquely identified at runtime by the combination of its fully qualified class name and the class loader instance that defined the class, there can be two versions of each exported class present in the JVM at the same time. But even if the class is the same, they are not in the same classloader, so cannot be cast to one another.

The solution here is to use the uses constraint. We specify a uses constraint on bundle B's export of package bar, declaring that package bar uses package foo. The metadata for this would be :

Export-Package: bar; version="1.0"; uses:="foo"

This adds a constraint on the framework dependency resolution for bundle C importing package bar. It must have in its class space the same foo package as bundle B. But wait...Bundle B import package foo with version 1.0, and bundle C explicitely imports package foo with version 2.0. So how does the framework resolves bundle C dependencies? Well...it can't. Bundle C cannot be resolved.

To solve this problem, one could just remove version attribute in bundle C import of package foo. Without the uses constraint, and if bundle B package bar does not expose package foo, then the dependency resolution algorithm will give foo in version 2.0 from bundle D to bundle C import (higher version first). But if B's bar package exposes package foo and declares a uses constraint on it, then the framework will resolve bundle C's import of package foo to the same one imported by bundle B : the package exported with version 1.0 by bundle A. The resulting situation is depicted in the schema below.
The uses contraint is shown by the little white rectangle linking the export and import of bundle B. We can see there that the foo package import has been resolved using bundle A's export, and that bundle C's class space now includes the foo package of bundle A. Resolution is now possible, and no ClassCastException is to be feared.

WARNING : Please note that too much uses contraints may restrict the framework choice of dependency resolution candidates and versions. You should be aware of this restriction. By the way, a uses constraint is not mandatory when two versions of the same class can co-exist (by not using methods exposing the class)...but i agree this is a little dangerous.

No comments:

Post a Comment