Skip to content

Kura Semantic Versioning

Cristiano De Alti edited this page Apr 19, 2017 · 5 revisions

Introduction

As of Kura release TBD the Kura API, all the packages in the org.eclipse.kura.api and TBD bundles, has been annotated with OSGi R6 @ConsumerType and @ProviderType annotations to properly support OSGi Semantic Versioning.

Semantic Versioning establishes a set of conventions for API developers, consumers and producers allowing an API to evolve remaining compatible with consumers and, where this is not possible, to manifest a breaking change.

Proper OSGi semantic versioning can be achieved both manually and using tools.

API Consumers and Providers

An API can have consumers (also called clients or applications) and providers and there are many more consumers than providers.

In OSGi an API is typically a Java interface. OSGi has a strong preference for interfaces and tends to not use classes in an API. In practice POJOs are also very common in an API. When discussing Semantic Versioning it's easier to start with interfaces but the same principles can be applied to classes.

Consumers

Consumers are applications that import an API package to either use or implement an API. Using an API means calling methods of that API. This usage is the most common one. Implementing an API means implementing a Java interface of that API. A good example is the Kura CloudClientListener callback interface implemented by applications to receive messages and notifications.

Providers

Providers also import an API package but their role is to provide an implementation of that API to the consumers. An API used by consumers is implemented by providers and viceversa. However there is clearly a difference between these roles: there are a few providers of the API, often only one, while there are many consumers. Also, in an API there are many more interfaces that are meant to be used than those meant to be implemented by the consumers. This last point is very important and the key to understand Semantic Versioning.

Consumer Type

An API type (interface or class) intended to be implemented or extended by consumers.

OSGi R6 defines a special @ConsumerType annotation to let tools understand the intention.

In Kura these types are implemented by Kura application bundles or simply Kura apps and called by the Kura runtime.

Provider Type

An API type (interface or class) not intended to be implemented or extended by consumers.

OSGi R6 defines an @ProviderType annotation for this usage. This is an alternative approach to the Eclipse @noimplement or @noextend Javadoc tags.

In Kura these types are OSGi services provided by the Kura runtime and used (called) by Kura apps.

API Importers and Exporters

In OSGi both consumer and provider bundles have to import an API package in their MANIFEST to use or implement (or extend) the API.

Consumer and provider bundles are importers.

The API itself can be contained and exported by the same bundle as the provider but more typically is distributed as a separate bundle. And since the API, consumers and producers are developed and distributed separately, OSGi uses versions for imports and exports to prevent bad things from happening if consumers or providers are not compatible with the API.

Semantic Versioning Rules

The rules are straightforward.

  1. Consumers will import an API package with a range [major.minor, major+1.0) where major.minor is the version of the API package used at compile time to build the consumer
  2. Providers will import an API package with a range [major.minor, major.minor+1) where major.minor is the version of the API package used at compile time to build the provider
  3. Every time a new version of the API package is released, if the change is such to break compatibility for consumers, its major number is incremented. Otherwise only the minor number is incremented and, as we will see, this result is a breaking change for providers.

The OSGi Semantic Versioning scheme above allows an API to evolve and still remain backward compatible with consumers. For example, if a new method is added to an @ProviderType interface, the API consumers will not be affected by the change. Such a change must be instead considered a breaking change for providers because to honor the API contract they must implement the new method.

In the first case, a breaking change for providers, if the new API was deployed in the OSGi framework, the provider bundle would not even resolve so, even if the consumer bundle would resolve its imports, it will later fail to get a service reference to the provider and the problem will be immediately noticed.

What happens if a method is added to an @ConsumerType interface? This must be considered a breaking change for consumers other than providers. If the API only incremented its MINOR number, the import of an old consumer would be resolved. However this consumer implements the old interface while a new provider will use the new interface. When the provider calls the new method an NoSuchMethodError will be thrown (from the provider call stack). To prevent this, the only sensible thing to do is to increment the API MAJOR number. Old consumers will not be able to resolve their import.

Example

A Kura app has been built against version 1.0.0 of the org.eclipse.kura.cloud package. The app bundle will import the package with the following range

Import-Package: org.eclipse.kura.cloud;version="[1.0, 2.0)"

The Kura runtime has been built against version 1.1.0 of the org.eclipse.kura.cloud package. The Kura runtime bundle will import the package with the following range

Import-Package: org.eclipse.kura.cloud;version="[1.1, 1.2)"

The app and the runtime will work happily together for every version of the org.eclipse.kura.cloud API package in the range [1.1, 1.2). For example if the org.eclipse.kura.api bundle exports the package as follows:

Export-Package: org.eclipse.kura.cloud;version="1.1.0"

Remember: API, consumers and producers are typically compiled at different times and distributed separately.

At a certain point the org.eclipse.kura.cloud API package is changed. Depending on the change either the major or the minor version of the package must be updated.

Based on the above rules adding a new method to the CloudService interface (an @ProviderType type) is compatible with Kura apps but a breaking change for the Kura runtime. Hence the org.eclipse.kura.api bundle will export the package with the new version 1.2.0:

Export-Package: org.eclipse.kura.cloud;version="1.2.0"

To deploy the new API, all providers must be updated to implement the new method and then reinstalled. They will have to import the package with a new range:

Import-Package: org.eclipse.kura.cloud;version="[1.2, 1.3)"

Kura apps will not be affected by this change and they don't need to be recompiled or reinstalled.

If instead a new method was added to the CloudClientListener callback interface (an @ConsumerType type), this would be a breaking change for consumers (other than providers). Hence the org.eclipse.kura.api bundle will export the package with the new version 2.0.0:

Export-Package: org.eclipse.kura.cloud;version="2.0.0"

All consumers must be updated to implement the new method and must be reinstalled/redeployed. They will have to import the package with a new range:

Import-Package: org.eclipse.kura.cloud;version="[2.0, 3.0)"

API Developers

We have shown that, while it's easy to add new methods to an @ProviderType type, adding methods to an @ConsumerType type always result in a breaking change for consumers.

Since adding a method to an existing @ConsumerType type is not a good idea, it's better to define an entirely new @ConsumerType type with that method when this cannot be avoided. If this type is added to an existing package, it represents a MINOR change for that package.

Note that a type which is not annotated is assumed to be an @ConsumerType by the API baseline tool. While this is the safest (and worst-case) assumption, in most of the cases it's wrong. @ConsumerType types are rare. An human is generally able to understand if a type is meant to be implemented by consumers from the Javadoc even if the type is not annotated but a tool is not that smart.

So every new type MUST be explicitly annotated either with @ConsumerType or @ProviderType (this is a mandatory check of the API review).

Clone this wiki locally