Hi Henning,
My team also face such fast-pace evolution that sometime impose quick
modifications to APIs. The real problem is not the lack of thinking we put in
designing these extended API, but the fact that these API extensions might be
required at a time when it would be unacceptable to force immediate upgrade of
all related bundles. Of course, simply not allowing unordered upgrades is, from
a technical point of view, the best and simplest approach. But sometimes, the
pressure to quickly provide a "low risk" upgrade to existing feature is simply
too high, be it the "time to market" factor, or whatever situation we may have
with a specific customer. In our case, being able to do so is actually a
business requirement.
Let me share with you an overview of how some development practices we have
established in order to better to deal with these situations. The key
strategies are:
Heavily split bundles in API vs Implementations bundles;
Design "set-of-features" add-on interfaces whenever a fundamental interface
need to be upgraded;
Fork projects when a bundle is subject to external evolution factors (or has to
be versionized simultaneously on two axis);
Testing API is even more important than testing implementations.
The first point states that, in most case, a bundle should either offer an API
or an implementation, but not both at the same time. I say in most cases,
though, because "utilities" bundles and some clearly delimited bundles might
not need such precautions. But not splitting enough tends to hurt more than
splitting too much, so in case of doubts, better split.
API bundles (and packages…) are semantically versionnized. API classes and
interfaces grow from a minor to the next one, and can eventually be
consolidated at major, for example to fix up any mistakes that might have been
introduced over time, or to remove methods that have become deprecated. Note
that given the semantic versioning rules, API bundles can almost never be
altered at micro level.
The main down side of isolating API is that it becomes impossible for a bundle
to directly instantiate (or extends) classes provided by implementation
bundles. This is easily fixed however by defining
factory/repository/manager/whatever else interfaces in the API, and having the
implementation bundle register its implementation of that interface as a
service. As for inheritance, it is considered a bad practice anyway.
Let's move on to the second point: we must be very strict in how API interfaces
and classes evolve. Basically, we consider that most API interfaces are
unalterable within the scope of a minor upgrade. This is a common best practice
whenever an interface is intended to be implemented by several external
bundles, because it may be impossible to track with certainty all
implementations of the interface, which therefore present risk of binary
incompatibilities at runtime.
However, we also consider as unalterable many interfaces not intended to be
publicly implemented, such as interfaces of services exposed through OSGi.
Instead, when new methods are required, we define a new interface that extends
the original interface (in general, we define those within the scope of the
original interface, simply to make it easier to track all extensions that have
been developed). For example, if we defined interface "IProductStore" in the
API bundle c.x.productstores v1.0.0, then we shall not alter that interface
before v2.0.0 of the API. If we eventually need to add more methods in order to
support a new feature, then we would define them in a new interface, say
IManageableProductStore (that extends IProductStore). That would be API bundle
c.x.productstores v.1.1.0. Then in v1.2.0, we might have
ISearcheableProductStore (also extending IProductStore), which would introduce
two more methods.
The real gain in doing so is that we might independently delay upgrading to a
specific feature, on both side of the API (that is, implementers and users).
For example, if for some reason, we consider that one implementation of
IProductStore can't easily be transitioned to support the
IManageableProductStore, then it may still support the ISearcheableProductStore
interface. Obviously, this push some of the difficulties of upgrades back to
the code using the API, since programmers must now be careful to check if a
given object implements the extended interface before invoking some extended
feature. However, this is generally a very descent trade off: first, there are
in general few users of those extended API (otherwise, it is probable that the
methods would have been identified early during development). Second, this is
new code anyway, so all places where checks has to be performed are known
anyway. And third, there are tons of very nice idioms to efficiently deal with
these "test before acting" sequences.
Note that this solution can trivially be implemented using the "instance of"
operator, followed by casting objects to the extended class. However, we prefer
to define a method "adapt(Class<X> clazz) : X" in our base interfaces, which
allow more dynamic decisions on either a given feature should be supported or
not. In the previous example, a specific implementation of IProductStore might
accept to be adapted to IManageableProductStore only if the connected user
possess administration rights. Or the code might look at company-level settings
to decide either a given feature is enabled or not. This is both cleaner and
safer than having the code using the API deal with permissions and
"company-wide" settings.
Now up to the third point… As we described earlier, mapping evolution of an API
bundle in a semantic versioning scheme is straight forward. In general,
implementation bundles can also be trivially mapped into semantic versioning
scheme (I won't elaborate here). However, we note that in some situations, the
versioning of a bundle may be affected not only by internal corrections and API
additions, but also by external factors.
Such external factors are common when a bundle ensure compatibility with an
external subsystem: there might be several major version of a key external
application, the customer database may be frozen to an older schema version,
and so on. In simple case, these external factors might easily be dealt by "if"
statements and dynamic checks at runtime, or possibly by having a few distinct
classes inside a same bundle. However, in more complex situations, it may be
tempting to produce distinct versions of the bundle. To illustrate this case,
let assume that bundle c.x.fileimport.appa provides a connector to read files
produced by an external program named "Application A - 1.0". Now if that
application file format changes in release 2.0, one might think that the bundle
should equivalently be upgraded to "2.0". Yet, assuming that support for file
format 1 and file format 2 can't reasonably be implemented in the same bundle,
then changing the bundle version number would be inappropriate: doing so would
prevent the bundle for format 1 from later being upgraded to a new major API,
since that would require that the implementation bundle's version be also
upgraded to the next major number, which is no longer available. In these
cases, the correct approach (still assuming that both formats can't reasonably
be implemented in a same bundle) would be to fork the original bundle into a
second one, with a distinct id and a distinct version number. That could be for
example c.x.fileimport.appa.format2 or c.x.fileimport.appa_v2.
Hopefully, this example is easily understood and accepted. However, we have
found that the same logic holds in many cases that are much less trivial. For
example, for reasons that I won't elaborate here, our database schemas
evolution is not coordinated with our main software releases. This means that
at the time the software connects to the database, the schema version is
checked, and then the bundle specific to the schema version is used thereafter.
However, because evolution of the implementation bundles happens on two axes
(specifically, the API version and the schema version), it would be problematic
to express semantic evolution through the classical version numbers. Therefore,
we create distinct bundles for each schema version, named something like
c.x.persistence.ourdatabase._3_141 4.2.5, where the schema version is 3.141,
the implemented API is 4.2, and there has been 5 corrections to the code since
the last upgrade to the API.
This strategy can be extended to other cases where external factors make it
impossible to allow a bundle evolution by following a simpler path. For
example, we sometimes judge that, for exceptional reasons, it is preferable for
a specific bundle to be temporarily forked for the needs of a specific
customer. Doing this produce a totally distinct bundle, let's say
c.x.somebundle.customizations.custumerabc, with distinct version numbers. This
is possible because anyway, no one has direct requirements for the original
bundle (c.x.somebundle.impl).
Obviously, systematic forking has some drawback, most notably that it makes the
code harder to maintain; fortunately, these forks are generally very short
lived. For example, we rarely have to support more than 2 or 3 schema versions
backward, and we only allow a customer specific branch to live until the end of
a "customer production cycle" (which rarely span more than 3 months, during
which we have to minimize risks for that specific customer). As soon as that
customer's production cycle is completed, we transition him back to the main
branch.
Together, the previous strategies ensure that we always have a few possible
solutions to deal with customer specific evolutions. We may for example fork
bundles (usually not the preferred path, but it is sometimes unavoidable), or
introduce new configuration items (which may be a good approach when we believe
that several customers might be interested in that same customization, and it
appears that the customization will remain pertinent in the long term), or
define a cleaner customization end point (if the customization appears
legitimate in the long term, and we believe that similar customizations will be
required for other customers), and so on. Also, even if we choose to introduce
a new, customer specific bundle, we might prefer to do so as a fragment, or to
avoid code duplication by having the replacement service (registered with an
higher ranking) capturing the service exported by the original implementation
bundle, and then delegate or decorate method calls to the original service
(rather than reimplement all the code).
In all case, we must carefully weight the pros and cons of each solution for
any specific issue. Maintainability and correctness are both majors issues with
these customizations. For this reason, I always question the time to live of a
customization and the probability that both the original code and the
customized code will have to evolve again during that time span.
The last point basically states that we should always have
implementation-independent tests, that validate that implementations do indeed
honour the API contract. It is obviously not always possible to test all aspect
of an implementation bundle in this way, but these implementation-independant
tests are particularly important once we deal with multiple forks of a base
implementation. Indeed, back porting tests between each customer-specific
branches of a base implementation is cumbersome. Testing to API definitely
reduce the maintenance cost of customer specific branches.
I hope this helps,
James Watkins-Harvey
On 2013-01-25, at 09h54, Henning Andersen wrote:
> Hi OSGi developers,
>
> we have been using OSGi for 3 years now, however without semantic versioning.
> We now want to take our versioning strategy to the next level by introducing
> semantic versioning and allowing individual release cycles for different
> components in our system.
>
> One of the issues we are struggling with is how to handle branches and
> versioning, especially in relation to API changes.
>
> Consider a package p with a single class C, with a single method m:
>
> package p;
>
> public class C {
> public void m();
> }
>
> Now say, we released a bundle B in version 1.0, with this package in version
> 1.0. To allow us to bug fix, the bundle B is branched into a 1.0 branch too
> in our source code repository.
>
> Another bundle X has Import-Package: p; version="[1.0;2)" as per semantic
> versioning and is released as version 1.0.
>
> Development of class C continues and a new method is added:
>
> public class C {
> public void m();
> public void n();
> }
>
> This is released in a new version of B, bundle-version 1.1, package version
> 1.1.
>
> Now a customer has X 1.0 and B 1.0. They request a new feature to X, but
> reject to upgrade B. The feature in X requires an API extension to the class
> C. So we add this in the 1.0 branch:
>
> public class C {
> public void m();
> public void o();
> }
>
> If we were to follow semantic versioning, we should release this as B version
> 1.1, p version 1.1. However, these numbers are already occupied.
>
> So we have to break the principle and release them as B version 1.0.1 and p
> version 1.0.1.
>
> The developer of X now needs to import the package p in the right version.
> However, using: Import-Package: p; version="[1.0.1;2)" is not entirely right,
> since his code is not compatible with the 1.1 version.
>
> So far we have 5 solutions:
>
> 1. Do not allow it. Only allow extending the API in a new minor version
> following the latest released version.
> 2. Add an attribute to the export 'Export-Package: a; version=1.0.1; p.C.o =
> true' and do the same on 'Import-Package: a; version="[1.0.1,2)"; p.C.o =
> true'. Requires that we identify the dependency to method level.
> 3. Fix the version of a in X: 'Import-Package: a; version="[1.0.1,1.1)"'.
> This however has the implication that X need upgrade when we upgrade B.
> 4. Build two bundles out of X, one with 'Import-Package: a;
> version="[1.0.1,1.1)"' and one with 'Import-Package: a; version="[1.1.1,2)"'
> (assuming the method was also added in 1.1.1).
> 5. Branch X into two versions (much like 3), with different import versions
>
> We think some of these could work, but do wonder if others have run into the
> same issues or similar branching/versioning issues and how they solved it?
> Any input is highly appreciated.
>
> Thanks,
>
> Henning
>
>
> _______________________________________________
> OSGi Developer Mail List
> [email protected]
> https://mail.osgi.org/mailman/listinfo/osgi-dev
_______________________________________________
OSGi Developer Mail List
[email protected]
https://mail.osgi.org/mailman/listinfo/osgi-dev