Monday, November 8, 2010

OSGi declarative managed services and configuration

Here are the explanation of OSGi declarative Managed Service I have experienced on practice and tried to summarize. Example for Eclipse is here: example.


OSGi declarative services are powerful tools. They are implemented as simple POJO. Developer is not required to to take care about service life circle, but OSGi framework takes. Managed services extends a service with additional capability to be configured at runtime.


The Basics

A managed service implements (or provides) org.osgi.service.cm.ManagedService interface. As per OSGi specification:
"A Managed Service is used by a bundle that needs one configuration dictionary and is thus associated with one Configuration object in the Configuration Admin service" 
 OSGi Service Platform Service Compendium. 104.5
Basically, a managed service is supposed to be used where it is required to have a service with capability to be (re)configured at runtime. ManagedService  interface provides a single method for that purpose:
public void updated(Dictionary configuration);
It is important to mention, that managed service is a singleton, i.e. there is always only one instance of managed service in the running OSGi framework. All other services will be using the same instance of a service, but there might be more then one ServiceReference instances referring to the service.

Managed service is bound to a configuration dictionary which is created by Configuration Admin service on demand. There is no needs to create configuration object manually.

Managed service is identified by its PID property named service.pid. In case of declarative managed service, by default, component name is the component service PID as well if property service.pid has been omitted.

Managed service might provide meta information about it's parameter types by means of implementing MetaTypeProvider interface.

Once a service is registered, Configuration Admin calls its update() method either with dictionary retrieved from configuration store or with null if there is no available configuration object. Method update() might be called not immediately after registration, but later when Configuration Admin service started and registered.

Declarative managed service

Managed service is very easy to declare as service component. In this case OSGi framework takes care about instantiating and registering a service. Configuration Admin takes care about providing a service instance with configuration dictionary at runtime.
Let's consider a simple Randomizer service which provide a method to generate a random double number within interval [min, max]. Service interval limits are configuration parameters to be stored as configuration object in bundle's configuration store.

First, create interface:


package com.acme.bundles.interfaces;
public interface IRandomizer {
public double random();
}

The interface might have several different implementation.

Second, declare a service component in xml file stored in bundle's OSGI-INF folder:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" 
name="com.acme.service.randomizer"
immediate="false" 
modified="modified" 
activate="activate" 
deactivate="deactivate">

<implementation class="com.acme.services.Randomizer"/>
<service>
<provide interface="com.acme.bundles.interfaces.IRandomizer"/>
<provide interface="org.osgi.service.cm.ManagedService" />
</service>

<!-- property name="service.pid" value="com.acme.service.randomizer"/ -->
<property name="randomizer.limit.min" type="Double">100.0</property>

<property name="randomizer.limit.max" type="Double">500.0</property>
</scr:component>

if service.pid property is omitted, the name of the component will be used as service PID.

In order to instruct OSGi framework to read component declaration and load service at start up the bundle's manifest needs to be extended with the following line:

Service-Component: OSGI-INF/*


Third, implement interface:

public class Randomizer implements IRandomizer
, org.osgi.service.cm.ManagedService {

double mMin = 0.0;
double mMax = 1.0;


public void activate(ComponentContext context) throws Exception {
System.out.println("Activating " + this);
configure(context.getProperties());
System.out.println("Defaults: [" + mMin + ", " + mMax + "]");
}


public void deactivate(ComponentContext context) throws Exception {
System.out.println("Deactivating " + this);
}


@SuppressWarnings({ "rawtypes", "unchecked" })
public void updated(Dictionary config) throws ConfigurationException {
if (!configure(config))
return;

StringBuffer b = new StringBuffer("Updated " + this);
b.append(" [").append(mMin).append(", ").append(mMax).append("] :: [");
for (Enumeration<Object> en = config.keys(); en.hasMoreElements();) {
Object key = en.nextElement();
Object value = config.get(key);
b.append(" (").append(key).append("=").append(value).append(") ");
}
System.out.println(b.append("]").toString());
}


public void modified(Map<String, Object> config)
throws ConfigurationException {

try {
mMin = (Double) config.get("randomizer.limit.min");
mMax = (Double) config.get("randomizer.limit.max");
} catch (Exception ex) {
ex.printStackTrace();
}
StringBuffer b = new StringBuffer("Modified " + this);
System.out.println(b.append(" [").append(mMin)
.append(", ").append(mMax).append("]").toString());
}


private boolean configure(Dictionary config){
if(config == null)
return false;

Object min = config.get("randomizer.limit.min");
Object max = config.get("randomizer.limit.max");
if (min != null) {
if (min.getClass().isArray()) {
mMin = ((double[]) min)[0];
} else {
mMin = (Double) min;
}
}
if (max != null) {
if (max.getClass().isArray()) {
mMax = ((double[]) max)[0];
} else {
mMax = (Double) max;
}
}
return true;
}

public double random() {
System.out.println("Executing random on " + this 
+ " [" + mMin + ", " + mMax + "]");
return mMin + Math.random() * (mMax - mMin);
}
}

Method activate() always called after a service is instantiated. Method is provided with default configuration defined in component xml, if no configuration object is available. It means a service gets at least default configuration. Component xml allows to provide arrays as defaults therefore activate() method always gets arrays as values for properties, for instance:

<property name="some.property" type="Integer">1 2 3 4</property>

If there have been no configuration object found in configuration store, method updated() is called with  null dictionary. Opposite to activate() method, configuration dictionary passed to update() might have not arrays, but simply values as they have been configured some time before and stored as configuration object. Basically, there might be whatever stored in configuration since it is a simple Dictionary. (Metatype Service Specification is currently out of scope here.). Therefore above example simply check for property type.

Thus, default configuration is not provided via update() method, but via activate() mathod and might be accessed via context.getProperties().

If configuration object is available in configuration store, it will be also passed to activate() method instead of defaults from component xml.

Method updated() is called every time when service configuration is changed by means of calling ConfigurationAdmin::update() method (see below). There are two different behavior possible:
  1. If there is a method referenced by modified property of the component, OSGi framework updates existing instance of service with new configuration calling first it's update() and than modified() methods, providing both with new configuration values. In the example above, method modified() is referred via component xml. In this case no new object created, but existing instance is reconfigured. All service trackers that tracks the service are informed about configuration changes by means of executing their method modifiedService().
  2. If there is no method referenced by modified property of the component, OSGi framework first deactivate current service instance, create new service instance calling it's constructor, activate new instance calling it's activate() method and then calls it's update() method. Service trackers are  informed about changes in service properties as about new service, i.e. first traker's removedService() method called indicating a current instant being destroyed and then traker's addingService() method called indicating new service instance.
Thus, method modified() plays pretty important role in managed service life circle. With modified() defined OSGi framework keeps service instance simply reconfiguring it once configuration changes are requested. Without modified() defined OSGi framework creates new instance per configuration request at runtime deactivating currently running instance.

There are different behavior depending on service PID and related to service singleton nature:

  1. Component name and service.pid property value are the same, i.e. either service.pid property has been omitted in component xml or has the same value as component name as in the example above. In this case, by default any bundle can dynamically change service configuration via ConfigurationAdmin by PID/component name. Besides, both updated() and modified() methods are executed. If modified() does not defined in component xml it is ignored. 
  2. Component name and service.pid property value are different. In this case, first, only a bundle that owns a service can create configuration object. If configuration object does not exist, any requests from other bundles to re-configure a service will be simply ignored, but OSGi framework creates configuration object which become virtually unaccessible (?!). It means, no one bundle including owning bundle will be able to change configuration anymore. The configuration object must be created by owning bundle and must be managed by owning bundle to avoid any inconsistency in such configuration. (Note: Default permissionning  is considered here.).

The second behavior is given because any configuration object is bound by default to bundle location, but our managed service is a singleton and may have only single configuration object. However, if it is required to define service.pid other than component name, it is still possible to manage a single service configuration object explicitly set configuration location to null before making update() call on ConfigurationAdmin:


Configuration config = configAdmin.getConfiguration(PID);
Dictionary<String, Object> props = config.getProperties();
props.put(...);
props.put(...);
config.setBundleLocation(null);
config.update(props);


Fourth, due to asynchronous nature of service registration, access a service instance as usually via ServiceTracker:

private IRandomizer service;



tracker = new ServiceTracker(mContext, IRandomizer.class.getName()
, new ServiceTrackerCustomizer() {
public void removedService(ServiceReference reference, Object context) {
System.out.println("Removed service : " + reference);
}


public void modifiedService(ServiceReference reference, Object context) {
System.out.println("Modified service : " + reference);
}


public Object addingService(ServiceReference reference) {
System.out.println("Adding service REF     : " 
+ reference);
System.out.println("Adding service SERVICE : " 
+ mContext.getService(reference));
IRandomizer service = (IRandomizer) mContext.getService(reference);
return service;
}
});
tracker.open();



tracker.open() requests framework to instantiate and register an instance of IRandomizer singleton service. Framework instantiate managed service, calls it's activate() and then update() method either with configuration from the store or null. Instance of IRandomizer will be available once ServiceTracker's addingService has finished.

Fifth, access available services via tracker:

Object[] services = tracker.getServices();
if(services == null)
continue;
System.out.println("Services available" + services.length);
for (Object service : services) {
if (service != null)
System.out.println("Accessing " + service.getClass().getName() 
+ " :: " + ((IRandomizer)service).random());
}

Don't forget: the services might be not available immediately, therefore tracker's getService() and getServices() methods might return null.

Due to flexible componentization of OSGi, there could be several bundles used:
  • Interface IRandomizer could be deployed within bundle com.acme.bundles.interfaces;
  • Implementation(s) of IRandomizer and it's component xml could be deployed within bundle com.acme.bundles.random.simple and others;
  • Service consumer(s) could be deployed in bundle com.acme.bundles.app and others.

The above mentioned deployment options mean it is possible to define open service interface and implement and deploy one or more implementation within one or more bundles. Service implementations are designated by their PID.


Configuring Managed Service 

By default, there is no configuration service associated with a service. An object will be created with the first getConfiguration() method call on ConfigurationAdmin service. getConfiguration() method should be provided with service PID. In below sample the same PID as component xml defines is used:

ServiceReference caRef = mContext.getServiceReference(ConfigurationAdmin.class.getName());
String pid = "com.acme.service.randomizer";
if (caRef != null) {
ConfigurationAdmin configAdmin = (ConfigurationAdmin) mContext.getService(caRef);
Configuration config = configAdmin.getConfiguration(pid);
}

Once configuration object is obtained, service could be reconfigured:

Dictionary props = config.getProperties();
if (props == null) {
props = new Hashtable();
}
double min = Math.random() * 100;
props.put("randomizer.limit.min", Math.random() * 100.0d);
props.put("randomizer.limit.max", min + Math.random() * 100.0d);
//To ignore configuration object location
//config.setBundleLocation(null);
config.update(props);

Possible service and framework behavior depends on service PID value, existence of service modified() method and configuration object location is described above.

What is next

Next will be Factory Component example.

Links
http://dz.prosyst.com/pdoc/mbs_prof_6.1/um/framework/bundles/osgi/scr/scr.html
Example's Source Code

3 comments:

  1. I think I will become a great follower.Just want to say your story is striking. The clarity in your post is simply striking and i can take for granted you are an expert on this subject.

    ReplyDelete
  2. I think I will become a great follower.Just want to say your story is striking. The clarity in your post is simply striking and i can take for granted you are an expert on this subject.

    ReplyDelete
  3. Nice blog as it has detailed information.I got your blog while looking for Managed IT Service Perth

    ReplyDelete