Qwylt

Overview

Qwylt is a framework for interconnecting modules, within and across module systems—a module system fabric. It is composed of api and spi classes that: The framework does not provide a concrete implementation, and is therefore not a replacement for OSGi or any other existing module system.

Existing module systems may be wrapped by this abstraction layer without modification; however, to enable interoperation, their dependency resolution algorithms must be updated to use the provided abstractions.

The framework complements but does not require the 'module' access keyword feature proposed in JSR 294.

Syntax Agnostic Types

The framework makes no assumptions about syntax or data structure in a module system, and does no parsing; instead, module system implementations map their internal representations into a canonical type provided by the framework. For example, version numbers are mapped to the Version type, which supports ordered comparison but cannot be constructed directly from a String. Version ranges and expressions are supported in a similar fashion, as is all module metadata.

Storage and Discovery

Module metadata and content are accessed via the ModuleArchive type—content as ModuleResources. Archives are installed into a ModuleRepository, which supports discovery using a simple, extensible (and indexable) query mechanism. A repository instance will normally store archives from a single module system; however, multiple repositories can be federated so that a search may find results in any number of module systems. Repositories do not use a fixed "parent-delegation" model; rather, each instance is constructed with a policy type that enables any form of delegation and/or result filtering.

Services Model

TBD

Composition

Modules are generally loosely coupled to their dependencies, and so may have their classes defined by separate ClassLoader instances. However, in some cases it is desirable to partition one logical module into multiple physical ones, and have them joined into the same ClassLoader at runtime (e.g. package private classes or localization resources). The framework supports such "composites" by:
  1. Allowing each ModuleArchive to declare whether it is a member of a composite, and, if so, to define the set of all members.
  2. Providing an (spi) shared state class to bind them together during resolution.

Permisssions

A module can control the set of other modules that are allowed to import it or to share a ClassLoader:

Class Space Consistency

Modules provide a conceptual boundary around a set of classes, enabling isolation and new forms of access control (e.g. "module private"). At runtime, that boundary is normally defined by a ClassLoader instance. This is partially just convenience but is also a direct consequence of the JVM definition of the package private (default) access mode, where a "runtime package" is defined to be both the package name and a definining ClassLoader.

ClassLoaders provide a name space for classes. This "class space" normally extends beyond a single loader instance to include the class space(s) formed by other loaders to which that instance will delegate. The concept of a class space is directly represented in the framework by the ClassSpace type, and can be thought of as a view onto the set of all classes reachable from a given loader instance.

In a standard Java SE environment, partitioning of classes into separate ClassLoader instances is coarse grained, with most application classes lumped into a single "system" loader instance via the "classpath". In a modular system, partitioning is fine grained, allowing a great deal of flexibility in composing systems, and enabling multiple versions of modules to exist in the same process. However, this flexibility implies a significant increase in the number of ClassLoader instances in a single process.

Increasing the number of ClassLoader instances raises the likelihood of a category of errors that are rarely seen in an SE environment, but are well known in EE and modular systems: duplication errors. A duplication error (LinkageError or ClassCastException) can occur whenever there is more than one Class instance with the same name—which can only exist with multiple loaders—and are extremely difficult to diagnose and correct (see Class Visibility Errors for more detail).

The OSGi alliance addressed this problem in the R4 release of the core specification, describing a model for detecting class space "consistency" which this framework has adopted: each package exported by a module must declare the set of packages on which it depends (see ExportedPackage.getDependentPackages()). With this information, the framework is able to guard against duplication errors, either by choosing a common provider from multiple choices or failing early when one cannot be found (again, see Class Visibility Errors for more detail).

A ClassSpace is "consistent" when there is a single provider for each package. More formally:

Modules that share a dependency must select a common provider or fail; a shared dependency on package s exists between modules M1 and M2 when all of the following are true:

The framework spi provides a general model for imposing such constraints (ImportConstraint), and a specific implementation for this one: SharesDependentPackages.

Connection ("dependency resolution")

A module may contain types that are private to that module as well as types which are public and accessible to other modules; the latter are said to be "exported". Rather than a complete list of all exported types, a convenient shorthand is used instead: each package that contains one or more public types is listed as an exported package. A module may then declare a set of imports (dependencies), where each is either an individual package or an entire module (shorthand for "all exported packages"). ModuleArchive provides accessor methods for both exports and imports.

Import declarations may specify an exact version, a version range or version expression, and may even require specific attributes; together these form a constraint that the runtime must use to select a provider. The process of connecting each import declaration with a specific provider is often referred to as "dependency resolution"; the framework uses the term "connection".

Connection is the main function of the framework spi, ensuring either that all dependent classes are visible to a module or that a connection error is thrown before first use. The canonical types of the api provide a module system independent model on which the spi classes can act; the spi classes provide a default implementation of the constraint solving machinery that results in connection or failure.

Connection Overview

The connection process is initiated when getModule() is called on an archive that has not previously been connected. The calling thread is blocked until either the connection completes successfully and a Module instance can be created and returned, or fails with a ConnectionError.

The connection process must be orchestrated to ensure that multiple threads calling getModule() cannot deadlock. Orchestration is performed by a singleton ConnectionCoordinator, with a default implementation that uses a single thread for all processing. Each archive maintains connection state in a ModuleState instance, an abstract type which is implemented by specific module systems. The ModuleArchive.getModule() method submits the state to the coordinator, which then drives it and the state of any import candidates in a two-phase commit style process.

The connection process proceeds as follows:

  1. An ImportSpace is created for each state. This type is a ClassSpace implementation that enables construction and acts as a domain for constraint processing.
  2. Each Import is converted to a ModuleQuery, which is used to look up candidate archives from the ModuleRepository.
  3. The candidates are added to the ImportSpace, by package name. Any unconnected candidates are submitted to the coordinator for processing.
  4. Constraints are applied to the candidate connections, which may result in removal; a ConnectionError is thrown if all candidates for a given package name are removed.
  5. Once the coordinator has prepared all submitted state instances, it commits them all.
  6. Once committed, the coordinator marks each state as connected, which unblocks any thread waiting at getModule(). That thread then proceeds to create a Module instance, which is cached for any subsequent getModule() calls.
A committed ImportSpace is immutable and is used by a ClassLoader to delegate requests to the imported Module instances; the framework spi contains a standard implementation of this usage pattern: ModuleClassLoader.

Connection Constraints

The connection process imposes the following set of constraints by default, though a module system implementation may choose to impose a different set.
Default Connection Constraints
  1. A module must fail to connect if a required import cannot be satisfied.
  2. A failed module must not be used to satisfy imports.
  3. Importing a module M is equivalent to importing all packages exported by M.
  4. A module may declare that it both exports and imports package p; once connected it may either export or import p, not both.
  5. If multiple providers exist for an import, connected providers are preferred over unconnected, and higher versions over lower.
  6. Modules that share a dependency must select a common provider or fail; a shared dependency on package s exists between modules M1 and M2 when all of the following are true:
    • M1 imports package p from M2
    • Package p depends on package s
    • M1 either imports or exports package s


Qwylt

Send comments or questions to Bryan Atsatt