Applications are often dynamic. New functionality is added, old functionality is removed, but the system keeps running. Even if your system is not inherently dynamic in its steady state, it is dynamic at startup and shutdown. Without the ability to handle the incremental addition of collaborators, you have to manually ensure that all prerequisite elements—services, extensions, listeners—are available before they are needed. Managing start order is frustrating, cumbersome, and brittle.
We have seen dynamic behavior and mechanisms throughout the Toast example. OSGi services, Service Trackers, and SAT, discussed in Chapter 6, “Dynamic Services,” help considerably. Declarative Services, as discussed in Chapter 15, “Declarative Services,” and the Extension Registry covered in Chapter 16, “Extensions,” simplify dynamic behavior further. In this chapter we generalize the concept of dynamism and look at how it impacts common coding patterns. In particular, we do the following:
• Introduce the notion of dynamic awareness and dynamic enablement
• Outline techniques for handling the arrival and departure of bundles and services
• Detail the OSGi Extender Pattern and BundleTracker
• Look at the dynamics of OSGi startup and shutdown
This chapter discusses the unique challenges presented to developers as they attempt to handle and facilitate comings and goings in the environment around them. We first recap Toast and look at examples of dynamic tolerance. With that as a base, we identify some common dynamic scenarios and outline coding and design best practices.
Dynamism is more than decoupling and using services. Do not be lured into the belief that simply “using services” will make your system behave correctly in a dynamic environment. Even with all the mechanisms in OSGi and Equinox, it is still possible to get dynamism wrong. Concurrency issues, stale listeners, complex programming patterns, and lost changes all collude to cause system failure in the face of dynamic change. In short, being dynamic does not come for free—you must follow certain practices to make the most of these scenarios.
Toast as an application is dynamic at the domain level; that is, vehicles and users come and go as the system runs. This kind of dynamism is inherent in distributed systems. In addition, in Toast each machine can itself change over time. For example, software can be installed, updated, and uninstalled from vehicles as they are running. We need to ensure that Toast reacts correctly when these changes occur.
The first step is to understand how the elements of the system collaborate—the food chain. Beyond the details of installing functionality, however, are the issues in getting the application to notice the arrival of new functionality and the management of interconnections when functionality is removed from the system—these are the topics addressed in this chapter.
Figure 21-1 gives a notional outline of the Toast Client bundles and shows how they relate to each other. GPS, Airbag, and Display are all at the bottom of the food chain. Tracking and Emergency are in the middle of the food chain, producing and consuming services, and the various UI bundles are at the top as they consume only services.
Bundles at the bottom of the heap are quite independent of others coming and going. They do, of course, need to clean up after themselves but are otherwise free to be introverted. Bundles in the middle and on top, however, must not only be good citizens and clean up but also carefully watch for other bundles and services coming and going.
Notice that it is sometimes interesting to talk about the grouping of logical functionality. For example, the figure shows that the Navigation application constituent parts interact closely and the elements of the Emergency application interact together, but the applications do not directly interact. Looking at the system more abstractly allows you to talk about the level of interaction between the various applications. This feeds into our food-chain notion but at a coarse grain.
The Toast bundles register services and listeners and contribute extensions to be instantiated by the system. In particular, Toast has the following requirements:
• Various back end bundles must register servlets with the HttpService
when it is available.
• All bundles must clean up listeners on removal.
• The back end portal must discover and integrate new actions as they arrive and clean up when they are removed.
• The client UI must react to the coming and going of client applications.
• All applications must correctly react to the presence or absence of their required services.
The rest of this chapter looks at how to handle each of these requirements, both in the context of Toast and the HttpService
and in more general scenarios, so that you can apply them to your domain.
Being dynamic is all about managing the links between types, their instances, and bundles—collaborators. There are two main challenges when trying to operate in a dynamic world: being dynamic-aware and being dynamic-enabled.
Dynamic awareness is an outward involvement, ensuring that the links you have to others are updated as collaborators are added to and removed from the system. Awareness is tricky to get right. As such, most of this chapter outlines techniques and helpers for making your bundles dynamic-aware.
Being dynamic-enabled relates to cleaning up after yourself. This is relatively straightforward to achieve because it’s a self-centered concern—you just need to do it.
Dynamic awareness has to do with updating your bundle’s data structures and behavior in response to changes in the set of collaborators; that is, a dynamic-aware bundle is one that can handle other elements coming and going or starting and stopping. Dynamic awareness needs to be considered wherever you have a data structure that is based on type, object, or data contributions from other bundles. In the Toast case, everything is dynamic. Vehicle devices such as GPS and airbags, software such as emergency management and audio control, can all come and go at any time. On the back end, portal actions and servlets are also dynamic. In short, all of Toast must be dynamic-aware.
Dynamic awareness comes in two flavors: addition and removal. We say that a bundle is dynamic-aware for addition if it is set up to handle the dynamic addition of collaborators to the system. Similarly, we say that a bundle is dynamic-aware for removal if it can handle the dynamic removal of collaborators from the system.
Addition is generally easier to deal with as there is less cleaning up to be done—structures can simply be augmented, caches flushed, or new capabilities discovered on the fly. Handling the removal of relationships may be as easy as flushing a cache, or it may be as complicated as tracking contributed, registered, or constructed objects, deleting them as required, and cleaning up afterward.
This cleanup is important because an uninstalled bundle is not garbage-collected until all instances of its types are collected and all references to its types are dropped; only then can the bundle’s classes be unloaded and the bundle be considered truly uninstalled. Technically, you can continue using existing types and objects even after the defining bundle is uninstalled, but the state of the bundle, and thus the integrity of its functionality and services, is not guaranteed—yet another reason why dynamic awareness is important. The OSGi specification calls these zombie bundles.
Using OSGi services, in and of itself, does not make your systems dynamic-aware—it is easy to write bad code that ignores the service lifecycle signals given by the framework, or neglects to unget services or unregister listeners. Similarly, the Equinox Extension Registry can be abused to create unstable dynamic systems. The key to being dynamic is using the supplied dynamic management mechanisms effectively.
Chapter 6, “Dynamic Services,” highlights several approaches for managing dynamic services. The dynamic service and extension usage patterns outlined in Sections 6.4, “Using Declarative Services,” and 16.6, “Dynamic Extension Scenarios,” are very powerful approaches. Subsequent sections in this chapter highlight some additional pitfalls and techniques for managing collaboration, such as contributed objects (e.g., listeners) and tracking bundle events.
Many systems include listeners or other objects, observers, that form couplings by being registered with a notifier service, the subject. The HttpService
is one example—servlets in effect are registered as clients of the HttpService
and are notified when there is a request to process. Every time a listener is added, a link between the listener and the notifier is created. If the listener’s contributor disappears or is deactivated, this link needs to be cleaned up.
In a perfect world, all clients would be dynamic-enabled and would clean up after themselves. Failure to clean up listeners prevents the uninstalled contributor from being garbage-collected and may cause runtime errors when a decommissioned listener is invoked. Here are a few strategies you can use to handle contributed objects:
Ignore—Assume that everyone is a good citizen and code your notifier robustly to handle any errors that might occur when notifying a stale listener. This tolerates the removal of the contributing bundle but leaves dangling listeners and has the potential to leak memory.
Validity testing—Include a validity test in your listener API. Before notifying any listener, the notifier tests its validity. Invalid listeners are removed from the listener list. The registering client then invalidates all its listeners when it is stopped. This lazily removes the listeners but still has the potential to leak if the notifier never tries to broadcast to, and thus test the validity of, an invalid listener. It also creates a standard test-and-set race condition on the listener’s validity flag.
Weak listener list—Using a weak data structure such as WeakReferences
or SoftReferences
to maintain the listener list allows defunct listeners to simply disappear. Since clients typically have to maintain strong references to their listeners to support unregistering, there is little danger of the listeners being prematurely garbage-collected.
Co-register the contributor—Rather than just registering the listener, have clients register both themselves and the listener. Event sources then listen for bundle events and proactively remove listeners contributed by bundles being removed.
Introspection—Every object has a class. The bundle for a class can be found using FrameworkUtil.getBundle(listener.getClass())
. With this information, you can tweak the co-registration approach to use introspection and cleanup. This approach is transparent but can be a bit costly and does not catch cases where one bundle adds a listener that is an instance of a class from a different bundle.
Discovery—The OSGi Whiteboard Pattern discussed in Chapter 15, “Declarative Services,” flips the relationship between observer and subject such that observers do not register with the subject but rather the subject discovers the observers when needed. This centralizes the list management and simplifies the cleanup but does not eliminate the issues altogether.
In the end, there is no one right answer. The different strategies have different characteristics. The point is that you must be aware of the inter-bundle linkages and make explicit decisions about how they are managed. You should choose the coding patterns that best suit your requirements (speed, space, complexity) and apply them consistently and thoroughly.
BundleListener
s are a powerful OSGi mechanism for handling change in a running system. Whenever a bundle changes state in the system, all registered BundleListener
s are notified. Listeners typically do the same sort of cache management described earlier and as shown in the following snippet:
In this case, the listener is registered as soon as the bundle is started. Your code collects and caches references related to bundles as needed. The BundleListener
watches for UNINSTALLED
and UNRESOLVED
bundle events and removes the data related to the affected bundle from the cache it is managing. Notice that this code is a good citizen as it removes its listener when the bundle is stopped.
BundleTracker
Services are not the only things that come and go in OSGi. As we have seen, bundles also change state. Reacting to these state changes can be quite powerful. The BundleListener
described in the preceding section is a simple example of this. Unfortunately, programming raw BundleListener
s is fraught with the same sort of complexity as using ServiceListener
s, as we touched on in Chapter 6, “Dynamic Services.” Enter BundleTracker
s.
The BundleTracker
class is a direct spin-off of ServiceTracker
, complete with customizers. In essence it allows you to describe the kinds of bundles in which you are interested and then presents you with a collection of the bundles that currently fit that model and a set of events relaying changes in the collection. Very convenient.
One of the key uses of BundleTracker
is to implement the Extender Pattern. The Extender Pattern is reminiscent of the Whiteboard Pattern. It allows an extender bundle to adapt discovered bundles and configure them into different contexts. OSGi and Equinox include several examples of this:
• The Declarative Services extender watches for active and starting bundles that have the Service-Component
header in their manifest. When it finds one, it parses the listed files and creates the described components.
• The Equinox Extension Registry is an extender that watches for resolved bundles that contain a plugin.xml
file in their root folder. The content of this file is loaded and woven into the Extension Registry.
The Extender Pattern itself is very useful, and BundleTracker
s make extender implementation reasonably straightforward.
Dynamic enablement means being a good bundle citizen—a dynamic-enabled bundle is written to correctly handle its own dynamic addition and removal. If you don’t clean up, you become a leak. Leaks bloat the system and eventually cause it to run out of memory or become intolerably slow. In the case of Toast, this means that all bundles must correctly unregister their listeners, unget acquired services, and unregister all their services.
Some of this can be done automatically for you with mechanisms such as Declarative Services. Being dynamic-enabled may also mean disposing of OS resources. The OS does not know when a bundle is stopped. To the OS, the JVM is still running, so it has to maintain all resources allocated to the JVM process. These include
• Open streams
• Undisposed graphical objects such as images, colors, and fonts
• Unstarted and running threads
• Open sockets
For a bundle to be dynamic-enabled, it must clean up any such resources as they are removed or stopped.
Developers often assume that when their bundle’s stop
method is called, the system is shutting down. As such, they do only mild cleanup, if any at all. These bundles are not dynamic-enabled. Bundles can be stopped at any time and actual system shutdown may not occur for quite some time.
A dynamic-enabled bundle is one that
• Ensures that all objects it registers are unregistered when no longer needed
• Ensures that all allocated resources are freed
• Implements a rigorous component deactivate
method where Declarative Services are used
• Implements a rigorous stop
method where a BundleActivator
is used
Bundles that register listeners, handlers, and UI contributions via code or allocate shared resources must take care to unregister or dispose of such objects when they are obsolete. This is just good programming practice. If you call an add
method, ensure that you call the matching remove
when appropriate. Similarly, this should be done for alloc
and free
, create
and dispose
, as well as for opening and closing streams.
To implement a backstop, your bundle activator or DS component needs to know which objects to dispose. This can be hard-coded if the set is limited and known ahead of time. For example, if your bundle holds a socket open, ensure that it is closed in the stop
method.
More generally, you can track the objects needing disposal. The following code is a sketch of how this works. The activator maintains a weak set of objects that need disposal. Throughout the life of the bundle, various disposable objects are added to and removed from the set. When the bundle is finally stopped, all remaining disposable objects are disposed. The set is weak to avoid leaks in situations where an object is added but not removed, even though it is no longer live.
This is just an example of an implementation approach. At the end of the snippet, there is an example of a registry change listener that lists itself as a disposable on creation. You can register the disposable at any point as long as it is added before the bundle stops. When the bundle stops, the listener is guaranteed to be removed from the event source—the Extension Registry in this case. If the listener is removed from the source and not the disposal list, it is either removed transparently as garbage or it is unregistered from the source when the bundle is stopped. Unregistering a listener that is not registered is a no-op.
Much of the focus on dynamism tends to be in terms of things being added or removed while the application is running. Some people assume that since their scenario is fixed, dynamic issues do not apply to them. In fact, every system is dynamic since bundles are installed and started in a sequential and more or less random order.
OSGi includes the notion of start level to help manage the startup and shutdown order. The start level of a bundle is set using the StartLevel
service. OSGi start levels are much like UNIX start levels. As the system starts, the start level is increased, and all bundles marked with the current start level are started before moving on to the next level. So, for example, by the time bundles at level 4 are started, all those needing to be started at level 3 have been started. If a level is not specified, the framework uses the default start level. In Equinox the default start level is 4. You can change this by manipulating the value of the osgi.bundles.defaultStartLevel
system property.
You can inject your bundle into the startup sequence by controlling its start level. This allows you to, for example, add login prompters before the application is run, control the bundles that are installed, or do last-minute cleanup as the system shuts down.
By far the best approach to startup dynamism is to simply use the available dynamic mechanisms as intended. If your bundle is dynamic-aware for addition, it does not need any special treatment at startup. When the needed services appear, the bundle or component will start operation accordingly. The use of Declarative Services and the Extension Registry with lazy activation greatly ease achieving this goal. Even so, with DS you may need to use immediate components, as discussed in Section 15.2.5, “Immediate Components.”
It takes some time for traditional Java programmers to get their heads around the OSGi pattern of interacting bundles. There is generally no main thread or entry point from which you can hang the startup of the various components. Even in situations where there is, that usage pattern is brittle and hard to maintain.
Many bundles start up and allocate resources that need to be cleaned up on shutdown. HTTP servers, worker pools, network connections, and the like all need to be explicitly shut down. An example from the writing of this book is telling. When putting together the client UI for the radio and CD player applications, we used the Eclipse Jobs mechanism to create workers to manage the operation of each virtual device. These workers wait
until an event is discovered, wake and process the event, and then schedule themselves to run and wait
again. When the system is shut down, the radio and CD player DS components are deactivated and need to clean up their Job
s. The following snippet outlines this process:
Logically, canceling a Job
causes it to stop what it is doing and return. This in turn would allow the join
to succeed, and we would be sure that the component’s resources were indeed freed. In practice, however, the Job
simply uses a wait
call and does not check for cancellation, as shown here:
Since the Job
does not look for cancellation, shutdown
’s call to cancel
has no effect, the Job
will never exit, and the join
call will never complete—deadlock. Instead, you must be careful to ensure that the component or bundle’s deactivation code is robust and complete—resources must be freed and the methods must not fail, do a partial job, or hang. In this case the problem can be addressed by having a volatile boolean
field called running
that is set and checked in the updated Job
code, as shown here:
The key here is that the Job
itself must be aware that it can be canceled and must clean itself up. The component can then cancel the Job
and be assured it is stopped. The same model can be applied to threads, loops, and many other resources allocated and managed by components or bundles.
Dynamic update and addition of functionality to running applications is an important part of the total user experience. Toast would be significantly diminished without it, and dynamic server-side applications would be impossible.
OSGi and Equinox include many mechanisms, from Declarative Services to BundleTracker
and ExtensionTracker
, in support of dynamic behavior. Even with these, being dynamic is not free. It is somewhat akin to concurrent programming. Attention to detail and revisiting and isolating your assumptions are good tactics—and likely good things to do anyway! In many cases, the outcome is a better, more flexible system that is also more dynamic.