Red Hat Training

A Red Hat training course is available for Red Hat Fuse

Chapter 3. Versioning

Abstract

For the stability of an OSGi application, it is essential that a bundle restricts the range of acceptable versions of its dependencies; otherwise, the bundle could be wired to an incompatible version of an imported package, with disastrous results. On the other hand, an application cannot afford to be too restrictive: a complex application might re-use a package in many different places. If a strict version restriction is placed on such a package every time it occurs, it might prove impossible to deploy the application, due to conflicting version requirements. Also, a certain amount of version flexibility is required in order to deploy patched bundles as hot fixes. This chapter describes the OSGi version policies that help you to achieve these goals and explains how to implement version polices using the Maven bundle plug-in.

3.1. Semantic Versioning

Overview

The basic principle of semantic versioning is that an increment to a major, minor, or micro part of a version signals a particular degree of compatibility or incompatibility with previous versions of a package. The Semantic Versioning technical white paper from the OSGi Alliance makes specific recommendations about how to interpret each of the major, minor, and micro parts of a version. Based on these definitions, the paper explains how to assign a version number to an exported package and how to assign a version range to an imported package.
This section summarizes the recommendations of the OSGi technical white paper.

Version fidelity

Consider a bundle (the importer) that has package dependencies on another bundle (the exporter). At build time, the importer is compiled and tested against a specific version of the exporter. It follows that the build-time version of the exporter bundle is the version least likely to cause any problems or bugs, because it has already been tested against the importer.
If you deploy the importer together with the original build-time version of the exporter, you have perfect version fidelity between the two bundles. Unfortunately, fidelity cannot usually be achieved in real deployments, because the exporter bundle is often used by many other bundles, compiled against slightly different versions of the exporter. Particularly in a modern open-source programming environment, where an application might pull in hundreds of bundles from many third-party projects and companies, it becomes essential to allow for version flexibility when matching bundles to their dependencies.

Backward compatibility in Java

To figure out how much version flexibility we can allow, we first need to consider the rules for backward compatibility in Java. Java is one of the few languages that explicitly considers issues of binary compatibility in its specification and the detailed rules can be found in the Binary Compatibility chapter of The Java Language Specification. The following is an incomplete list of changes that are binary compatibility with consumers of particular classes or interfaces:
  • Adding new fields, methods, or constructors to an existing class or interface.
  • Deleting private fields, methods, or constructors of a class.
  • Deleting package-only access fields, methods, or constructors of a class or interface.
  • Re-ordering the fields, methods, or constructors in an existing type declaration.
  • Moving a method upward in the class hierarchy.
  • Reordering the list of direct super-interfaces of a class or interface.
  • Inserting new class or interface types in the type hierarchy.

Consumers and providers

In a well-structured Java application, bundles often fall into one of the following categories: API, consumer, and provider. The consumer and provider roles can be defined as follows:
  • Consumer—a bundle that uses the functionality exposed by an API, invoking methods on the API's Java interfaces.
  • Provider—a bundle that provides the functionality of an API, containing classes that implement the API's Java interfaces.
Both the consumer and the provider import packages from an API bundle, as illustrated in Figure 3.1, “Consumers and Provider of API Interfaces”. Hence, the consumer and the provider are both sensitive to changes in the API bundle.

Figure 3.1. Consumers and Provider of API Interfaces

Consumers and Provider of API Interfaces
It turns out that the rules of binary compatibility are quite different for consumers and providers. The coupling between consumer, provider and API can be described as follows:
  • Compatibility between consumer and API—this coupling obeys the rules from the Binary Compatibility chapter of the Java Language Specification. There is quite a lot of scope for altering the API without breaking backward compatibility; in particular, it is possible to add methods to Java interfaces without breaking compatibility.
  • Compatibility between provider and API—this coupling is not explicitly considered in the Java Language Specification. It turns out that this coupling is much more restrictive than the consumer case. For example, adding methods to Java interfaces breaks backward compatibility with the provider.

Callback interfaces

The role of a consumer is not always as clear cut as initially suggested here. Although consumers mostly interact with an API by invoking methods on the API's interfaces, there are some cases where a consumer actually implements one of the API interfaces (thus muddying the distinction between a consumer and a provider). A classic example of where this might happen is when an API defines a callback interface, which a consumer can implement in order to receive event notifications.
For example, consider the javax.jms.MessageListener interface from the JMS API, which is defined as follows:
// Java
package javax.jms;

public interface MessageListener {
    void onMessage(javax.jms.Message message);
}
A consumer is expected to implement the onMessage() method in order to receive messages from the underlying JMS service. So, in this case, the more restrictive binary compatibility rules (the same ones that apply to a provider) must be applied to the MessageListener interface.
Note
It is recommended that you try to be as conservative as possible with callback interfaces, changing them as infrequently as possible, in order to avoid breaking binary compatibility with consumers.

OSGi version syntax

The OSGi version syntax defines a version to have up to four parts, as follows:
<major> [ '.' <minor> [ '.' <micro> [ '.' <qualifier> ]]]
Where <major>, <minor>, and <micro> are positive integers and <qualifier> is an arbitrary string. For example, the following are valid OSGi versions:
4
2.1
3.99.12.0
2.0.0.07-Feb-2011

OSGi version ranges

When declaring dependencies on imported packages in OSGi, you can declare a range of acceptable versions. An OSGi version range is defined using a notation borrowed from mathematics: square brackets—that is, [ and ]— denote an inclusive range; and parentheses—that is, ( and )—denote an exclusive range. You can also mix a parenthesis with a bracket to define a half-inclusive range. Here are some examples:
OSGi Version RangeRestriction on Version, v
[1.0, 2.0)1.0 <= v < 2.0
[1.0, 2.0]1.0 <= v <= 2.0
(1.4.1, 1.5.5)1.4.1 < v < 1.5.5
(1.5, 1.9]1.5 < v <= 1.9
1.01.0 <= v < ∞
Of the preceding examples, the most useful style is the half-inclusive range, [1.0, 2.0). A simple version number on its own, 1.0, is interpreted as an inclusive range up to positive infinity.

Semantic versioning rules

The fundamental idea of semantic versioning is that a bundle's version should indicate when the bundle breaks binary compatibility with its preceding version, bearing in mind that there are different kinds of binary compatibility: compatibility of consumers and compatibility of providers. The OSGi Semantic Versioning technical white paper proposes the following versioning conventions:
<major>
When a change breaks binary compabitility with both consumers and providers, increment the bundle's major version number. For example, a version change from 1.3 to 2.0 signals that the new bundle version is incompatible with older consumers and providers.
<minor>
When a change breaks binary compatibility with providers, but not consumers, increment the bundle's minor version number. For example, a version change from 1.3 to 1.4 signals that the new bundle version is incompatible with older providers, but compatible with older consumers.
<micro>
A change in micro version does not signal any backward compatibility issues. The micro version can be incremented for bug fixes that affect neither consumers nor providers of the API.
<qualifier>
The qualifier is typically used as a build identifier—for example, a time stamp.

Consumer import range

Assuming that an exporter bundle obeys the preceding OSGi semantic versioning rules, it is possible to work out the range of versions that are compatible with a consumer. For example, if a consumer is built against an exporter with version 1.3, the next version that would break binary compatibility with the consumer is 2.0. It follows that all exporter versions up to, but excluding, 2.0 ought to be compatible with the consumer.
In general, the consumer import range is calculated from the build-time version of the exporter according to the following rule: given the build-time version, <major>.<minor>.<micro>.<qual>, define the corresponding consumer import range to be [<major>.<minor>, <major>+1).
Table 3.1, “Example Consumer Import Ranges” shows some examples of how to choose the correct consumer import range for a given exporter version.

Table 3.1. Example Consumer Import Ranges

Build-Time Version of ExporterConsumer Import Range
3.0[3.0, 4)
2.0.1[2.0, 3)
2.1.4[2.1, 3)
2.1.5.2011-02-07-LATEST[2.1, 3)

Provider import range

According to the OSGi semantic versioning rules, providers are compatible with a narrower range of versions than consumers. For example, if a provider is built against an exporter with version 1.3, the next version that would break binary compatibility with the provider is 1.4. It follows that all exporter versions up to, but excluding, 1.4 ought to be compatible with the provider.
In general, the provider import range is calculated from the build-time version of the exporter according to the following rule: given the build-time version, <major>.<minor>.<micro>.<qual>, define the corresponding provider import range to be [<major>.<minor>, <major>.<minor>+1).
Table 3.2, “Example Provider Import Ranges” shows some examples of how to choose the correct provider import range for a given exporter version.

Table 3.2. Example Provider Import Ranges

Build-Time Version of ExporterProvider Import Range
3.0[3.0, 3.1)
2.0.1[2.0, 2.1)
2.1.4[2.1, 2.2)
2.1.5.2011-02-07-LATEST[2.1, 2.2)