Thursday, April 24, 2008

JSR 277 Interoperation

We all know that JSR 277 is supposed to "interoperate" with OSGi, but the details have not yet been specifically addressed in the spec or EG. And many view this as a critical missing element: Glyn Normington has gone so far as to file a bug which has gotten a lot of attention (Glyn also reminded all of us that OSGi is not only a de facto standard but a proper one as well). Peter Kriens wonders if the JSR has become a ghost, and Neil Bartlett chronicles some of the events surrounding the "missing strawman", both underscoring the growing frustration in the OSGi community.

We'll see some activity in the EG on this very soon. But I want to make something clear: the bulk of the interoperability story is already present in the spec. No, it isn't called out as such, and yes, there are certainly some missing details, but the essentials are all there.

What the heck am I talking about? Well, lets explore what "interoperation" actually means here...

First, plant firmly in your brain that JSR 277 provides a framework under which multiple module implementations, called module "systems" in the spec, may co-exist in the same process. OSGi is obviously the one that many think of first, but there are others and we need to ensure that they can all play nice with each other. So clearly interoperation is concerned with the interactions between module systems. But this is far too fuzzy.

Second, consider the general problem of resolving dependencies among individual module instances (I'm using the term "module" in the generic sense here, think "bundle" when we're talking about OSGi). A module system has some means of expressing dependencies, and it is up to the runtime to satisfy ("resolve") each of them by finding an appropriate match out of the available modules. The expressions may be very loose (e.g. "package 'com.acme.dynamite', any version") or extremely rigid (e.g. "module 'foo', version 3.14, created and signed by the Apache foundation, for a specific version of an OS and processor).

Given a list of dependency expressions for a given module, an implementation must do the following for each:

  • Use the available lookup mechanisms to find candidate modules that satisfy the expression; fail if none.
  • If more than one candidate exists, use some interesting algorithm to choose one.
  • If that candidate has not itself been resolved, do so.

If successful, this recursive process results in a list of module instances (the "imports"), each of which has its own ClassLoader. And the module is now ready to satisfy class and resource lookups from its loader, by delegating to the imported loaders when required.

Ok, so it is relatively easy to see how this works within a single module system, where all of the modules are the same type and internal implementation classes can be assumed and used. But JSR 277 would not be very interesting or useful if modules could only resolve against other modules of the same type. And that, finally, is what interoperation means here: enabling dependency resolution and class/resource sharing across modules of different module systems.

And it highlights some obvious requirements for cross module system interoperation. For the dependency resolution steps, each implementation must be able to:

1. Map its dependency declarations into a standard runtime representation.
2. Support a standard search model over its stored modules, using the runtime dependency expressions.
3. Map its stored module data into a standard runtime representation that can be returned by the search.

Abstractions for these are already well defined: dependencies can be mapped into a
Query composed of arbitrary constraints (name, version, version ranges, attributes, etc). Queries can be passed to Repositories to find ModuleDefinitions, each of which can then be instantiated as a Module with its own class loader.

A single
Repository instance will (normally) support modules from only one module system, but searches can and will traverse multiple repositories. So, given a search that starts at an OSGi repository, for example, and which also visits a BryanModuleSystem repository, it is then very natural for a dependency expressed in an OSGi bundle to be resolved by a BryanModule.

Class and resource sharing requires that each implementation must:

4. Support a standard class loader delegation model.

For this, the public methods of
ClassLoader itself can be used: iterate the list of imported Module instances, grab the loader from each and call the appropriate public method. We can of course do a much better search by keeping a simple index that maps package names to the module(s) that export that package, thus avoiding a linear search across the imports (and the consequent fan out that would entail). Given that class loading often dominates startup time, and that search dominates class loading, this optimization turns out to be well worthwhile. (It is also quite natural when the primary dependency expression uses package rather than module name, but that is a topic for another day.)

Finally, lifecycle management must be addressed. Each implementation must:

5. Support standard install/uninstall operations.
6. Support standard "start" and "stop" operations.

Repository provides the interface for install and uninstall, ModuleDefinition provides the "start" operation (get instance method), and ModuleSystem provides "stop" operations (release/disable module methods).

So what's left? We need a solution to the "interesting algorithm to select among multiple candidates" problem. We may want a new loader delegation interface that refines/optimizes the contract. We need to ensure that the concurrency model in place during resolution works correctly across module systems. And I'm sure we'll uncover a few more such details as we gain experience with multiple implementations.

And we do need to nail down these issues as soon as possible.

But, overall, the "interop" story has all the main characters and much of their dialog already filled in.

No comments: