-
Notifications
You must be signed in to change notification settings - Fork 313
Kura Semantic Versioning
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.
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 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 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 mean 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.
An API type (interface or class) intended to be implemented or extended consumers.
OSGi R6 defines a special @ConsumerType
annotation to let tools understand the intention.
This is an alternative approach to the Eclipse @noimplement
or @noextend
Javadoc tags.
In Kura these types are implemented by Kura application bundles or simply Kura apps and
called by the Kura runtime.
An API type (interface or class) not intended to be implemented or extended consumers.
OSGi R6 defines an @ProviderType
annotation for this usage.
In Kura these types are OSGi services provided by Kura runtime and used (called) by Kura apps.
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.
The rules are straightforward.
- Consumers will import an API package with a range [major.minor, major+1.minor) where the major.minor is the version of the API package used at compile time to build the consumer
- Providers will import an API package with a range [major.minor, major.minor+1) where the major.minor is the version of the API package used at compile time to build the provider
- 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 too. If the API only incremented its MINOR number, 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 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.
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 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 @ConsumerType
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)"
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).
User Documentation: https://eclipse-kura.github.io/kura/. Found a problem? Open a new issue or start a discussion.