5.4. Dealing with Service Unregistration

If your service depends on the presence of another service, and there is a realistic chance that the other service may be unregistered at some point in time—perhaps because the service-providing bundle undergoes an update—you should program your service to protect yourself[1] from that event.

[1] That is, put yourself in the shoes of the client bundle in this chapter.

Such safeguard measures add quite a bit of complexity to your service, but the complexity is inherent in the component-based model. In the world of the OSGi framework, the earth does stand on top of giant tortoises, and you do not want to be thrown flat on your face when the ground moves away from under your feet!

The following sections list several solutions. Evaluate their advantages and disadvantages before deciding which one is the best for your application. We do not encourage you to use a single one for all your bundles.

5.4.1. Don't Start without the Service

When you have package dependency on another bundle, and the other bundle has exported the packages you declare to import, your bundle is resolved. However, it is possible, as we have explained in earlier sections, that the other bundle has not registered the service yet. Therefore, you should check this condition by verifying that the service reference you obtained is not null:

ServiceReference ref = bundleContext.getServiceReference(
   "com.acme.service.print.PrintService");
if (ref == null) {
   // the service has not been registered by the print bundle yet.
} else {
   PrintService svc = (PrintService) bundleContext.getService(ref);
}

Otherwise, you will get a NullPointerException exception when you call getService with a null service reference.

What can you do if the service has not been registered? If you are inside the activator's start method, you can simply throw Exception:

public class Activator implements BundleActivator {
   public void start(BundleContext ctxt) throws Exception {
      ServiceReference ref = ctxt.getServiceReference(
         "com.acme.service.print.PrintService");
      if (ref == null)
         throw new Exception("Required service " + ref +
               " has not been registered.");
      }
      ...
   }
   ...
}

As a result, your bundle won't start if the service you need is not registered. Taking this approach leaves the coordination responsibility to administrative software, which should then attempt to reactivate your bundle when it knows that the service is likely to have been registered.

This “solution” does not address the general problem of a service getting unregistered some time after you have obtained it, however. The situation is not unlike you telling your boss that you cannot start working unless a computer is provided; yet once you get started, it is still possible that your computer breaks down or is taken away.

5.4.2. Discovering Stale Service

After you succeed in obtaining the service by calling getService, you get a reference[2] to the service object itself. Some time elapses, and the registering bundle chooses to unregister the service. At this time, the service object in your possession is still a valid object in the Java virtual machine, and in all likelihood will continue to work. Figure 5.3 illustrates this situation.

[2] Don't confuse this with the service reference, which is an instance of ServiceReference.

Figure 5.3. Bundle B gets a reference to a service from the registry. Some time later, the service is unregistered, leaving B with a stale reference.


However, when the service-providing bundle unregisters its services, it may signify the client bundles, based on certain application logic, that they should no longer use the service. For example, when a printer runs out of paper, the print service may get unregistered; or a preference service may be unregistered when its back-end directory server needs an upgrade. Undesirable consequences may ensue if client bundles disregard this fact and continue to hold on to the service object they have acquired.

We say that the service object is stale if the service is unregistered some time after a client bundle has obtained it. Figure 5.4 illustrates the time line.

Figure 5.4. The time line of how a service obtained by the client bundle becomes stale


To find out whether a service has become stale, you can call the getBundle method on the service reference to the service:

if (serviceReference.getBundle() == null) {
   // the service referenced by serviceReference
   // has been unregistered
}

The getBundle method returns the bundle that has registered the service. If the service is unregistered at a later time, this method returns null.

5.4.3. Carrying On without the Service

In many applications, your bundle or service is tolerant enough to be able to continue functioning, even though the service on which you depend has been unregistered. It is a lot like even though we occasionally experience power outages, we can usually keep up with our daily routine, although less effectively.

In such situations, you need to program your bundle or service as a state machine with two states: how it should work when the needed service is available and what it should do when the service is not. Furthermore, you must listen to ServiceEvent to figure out whether the service in which you are interested is going away or has come back.

As an example, consider a memo service that uses the print service. The memo service enlists the help of a service event listener, ServiceMonitor, to monitor the status of the print service:

package com.acme.service.memo;
import org.osgi.framework.*;
import com.acme.service.print.PrintService;

class ServiceMonitor implements ServiceListener {
   static final String PRINT_SERVICE_NAME =
      "com.acme.service.print.PrintService";
   private BundleContext context;
   private ServiceReference currentRef = null;
   PrintService printService = null;

   ServiceMonitor(BundleContext ctxt) {
      this.context = ctxt;
   }

   /**
    * Monitor service events for the print service,
    * and flag its availability accordingly.
    */
   public synchronized void serviceChanged(ServiceEvent e) {
      if (e.getType() == ServiceEvent.UNREGISTERING) {
         if (e.getServiceReference().equals(currentRef)) {
            // the print service we are using is going away
            context.ungetService(currentRef);
            this.currentRef = null;
            this.printService = null;
         }
      } else if (e.getType() == ServiceEvent.REGISTERED) {
         // print service is registered
         if (this.printService == null) {
            this.currentRef = e.getServiceReference();
            this.printService = (PrintService)
               context.getService(currentRef);
         }
      }
   }
   synchronized void init() {
      try {
         context.addServiceListener(this,
            "(objectClass=" + PRINT_SERVICE_NAME +")");
      } catch (InvalidSyntaxException e) {
         // won't happen
      }
      ServiceReference ref =
         context.getServiceReference(PRINT_SERVICE_NAME);
      if (ref != null) {
         this.currentRef = ref;
         this.printService =
               (PrintService)context.getService(ref);
      }
   }
}

The monitor's job is quite obvious: It maintains a reference to the print service, nullifies it when the print service is being unregistered, and reestablishes it when the print service has been registered. Its init method sets the initial state of the print service.

Here is the activator for the memo service bundle:

package com.acme.service.memo;
import org.osgi.framework.*;

public class Activator implements BundleActivator {

   public void start(BundleContext ctxt) {
      ServiceMonitor monitor = new ServiceMonitor(ctxt);
      monitor.init();
      MemoService memo = new MemoServiceImpl(monitor);
      ctxt.registerService(
         "com.acme.service.memo.MemoService", memo, null);
   }

   public void stop(BundleContext ctxt) {
      // The listener will be removed automatically
   }
}

This bundle is always started regardless of whether the print service is registered. In the activator, we add the service listener that listens for service registration or unregistration events of com.acme.service.print.PrintService. As shown earlier, this is achieved by calling addServiceListener and by supplying an LDAP filter parameter to narrow the number of service events delivered to the listener.

The print service may have already been registered before our listener is added. We find this out by attempting to obtain the service and, if successful, initializing the internal state of the listener. Notice that this is done in a synchronized method to avoid a nasty race condition under which another thread may unregister the service right after we have checked the condition

if (ref != null)

but before we add the listener. Here is why: While we are inside the init method and the lock is being held on the service listener (monitor), the framework's attempt to call back to the listener's synchronized serviceChanged method from another thread is blocked. It is not able to deliver the ServiceEvent.UNREGISTERING event. This prevents the framework from finishing its unregistration routine, because it must wait for the synchronous delivery of ServiceEvent to return. As soon as we are done with the initialization and release the lock, the service listener is in place and is notified of the unregistration. If the init method is not synchronized, our listener may miss this unregistration notification, leaving the listener with an incorrect initial state.

We use currentRef to identify the service reference to the service currently in use. Beware if more than one service instance has registered with the specified service interface. In that case, ServiceMonitor is notified when a service under the same interface, but not used by your bundle, is unregistered. Generally, you can take two measures to rule out irrelevant service events:

  1. Use additional service properties in the filter. For example, suppose there are two print services, one for the local printer and the other for the network printer. If the network printer is the one you are using, you can add a differentiating condition, implType, to the filter:

    ctxt.addServiceListener(listener, "(&(objectClass=" +
          serviceName + ")(implType=network printer))");
    
  2. Make additional checks within the service listener's serviceChanged method, which is as follows:

    public void serviceChanged(ServiceEvent e) {
       ServiceReference ref = e.getServiceReference();
       if (!"network printer".equals(ref.getProperty("implType"))) {
          // not the instance of print service we are
          // interested in; ignore.
          return;
       }
       ...
    }
    

How would the memo service use the print service under the new circumstances? We have seen that the service monitor is passed into MemoServiceImpl when it is constructed in the activator. Thus it may call the print service with the following sequence:

synchronized (monitor) {
   if (monitor.printService != null) {
      monitor.printService.print(...);
   } else {
      display("The print service is temporarily unavailable." +
         " Try later!");
   }
}

Note that accessing and modifying the reference to the print service should be synchronized on the service listener, because while the memo service is checking the availability of the print service, another thread may be in the process of unregistering it.

In summary, to keep your bundle active while some service on which you depend comes and goes, you need to program your bundle with the following five guidelines in mind:

  1. Think through the application logic in two cases: what your bundle does when the needed service is registered and what is does when the needed service is unregistered.

  2. Set up ServiceEventListener to monitor the needed service. Maintain an availability condition in the listener. When the ServiceEvent.REGISTERED event happens, get the service; when the ServiceEvent.UNREGISTERING event happens, unget the service. Change the availability condition accordingly.

  3. On bundle activation, call getService to see whether the service has already been registered and initialize the availability condition.

  4. Synchronize on the event listener object whenever you need to check or modify the availability condition.

  5. In your code where you invoke methods on the service, check the availability condition first. Branch into two execution paths accordingly.

5.4.4. Picking an Alternative

Within the framework, multiple services with the same service interface may be present. They differ only in implementation. In this situation you may want to obtain another service instance when the one you have been using is getting unregistered. An analogy from daily life is to take the train when the plane is grounded.

Most of the code in the previous section is still applicable, because you still need to prepare yourself for the situation when all the needed services are unregistered. The following is the revised ServiceListener implementation. The code in bold type reflects the new behaviors:

package com.acme.service.memo;
import org.osgi.framework.*;
import com.acme.service.print.PrintService;

class ServiceMonitor implements ServiceListener {
   static final String PRINT_SERVICE_NAME =
      "com.acme.service.print.PrintService";
   private BundleContext context;
   private ServiceReference currentRef = null;
   PrintService printService = null;

   ServiceMonitor(BundleContext ctxt) {
      this.context = ctxt;
   }

   /**
    * Monitor service events for the print service,
    * and flag its availability accordingly.
    */
   public synchronized void serviceChanged(ServiceEvent e) {
      if (e.getType() == ServiceEvent.UNREGISTERING) {
         // print service is about to go away
         if (!e.getServiceReference().equals(currentRef)) {
            // it is not the print service we're using; ignore.
            return;
         }
         context.ungetService(currentRef);
         // pick an alternative
         this.currentRef =
            context.getServiceReference(PRINT_SERVICE_NAME);
         if (currentRef != null) {
            this.printService = (PrintService)
               context.getService(currentRef);
         } else {
            this.printService = null;
            this.currentRef = null;
         }
      } else if (e.getType() == ServiceEvent.REGISTERED) {
         // print service is registered
         if (this.printService != null) {
            // one more service instance is registered, but we
            // are happy with the one we have been using.
            return;
         }
         ServiceReference ref = e.getServiceReference();
         this.printService =
            (PrintService) context.getService(ref);
         this.currentRef = ref;
      }
   }

   synchronized void init() { ... }
}

The listener must remember the service reference representing the service instance currently being used. As in the previous example, this is maintained in currentRef. When we are notified that an instance of the print service is being unregistered, we first determine whether it is the instance we have been using. If it isn't, then the unregistration does not affect us. If it is, we must select an alternative. At this time, the outgoing service has already been unregistered. Therefore, we can simply get a service reference to another currently registered print service. A null return value means that none is available; otherwise, we switch to the new print service.

When we are notified that a print service is registered, we don't need to do anything if we are currently holding a valid service instance.

Taking this approach usually implies that the alternative service instances perform similar operations and have been designed to be used interchangeably. Their internal integrity must be preserved when any one of them takes over the task at some point in time. For example, suppose you have two log services that store records in a remote database for auditing purposes. One serves as the backup of the other. In this situation, time stamping each record allows you to get a complete trace after merging the entries separately recorded by the two service instances.

5.4.5. Cascading Service Registration

There are situations when the service on which you depend is unregistered, it is impossible for your bundle to continue. This is similar to the situation when your suppliers quit, you cannot open for business. If this is so, you may unregister the services provided by your bundle when the needed service is unregistered.

Suppose PrintService cannot print without FontService, which provides basic fonts and can install fancier ones. If the font service is unavailable, the presence of the print service may be misleading to its clients, which may choose to keep trying—in vain. If the print service is also unregistered, its clients may understand that the problem is not transient. PrintService monitors service events pertinent to FontService by adding an event listener as before.

The following is the revised service listener that monitors the font service to handle this scenario:

package com.acme.service.print;
import org.osgi.framework.*;
import com.acme.service.font.FontService;

class ServiceMonitor implements ServiceListener {
   static final String PRINT_SERVICE_NAME =
						"com.acme.service.print.PrintService";
						private ServiceRegistration printReg;
   private BundleContext context;
   private ServiceReference currentRef = null;
   FontService fontService = null;

   ServiceMonitor(BundleContext ctxt) {
      this.context = ctxt;
   }
   /**
    * Monitor service events for the font service,
    * and flag its availability accordingly.
   */
   public synchronized void serviceChanged(ServiceEvent e) {
      if (e.getType() == ServiceEvent.UNREGISTERING) {
         if (e.getServiceReference().equals(currentRef)) {
            // we withdraw PrintService because
						// needed FontService is going away
						if (printReg != null) {
						printReg.unregister();
						printReg = null;
						}
            context.ungetService(e.getServiceReference());
            this.currentRef = null;
            this.fontService = null;
         }
      } else if (e.getType() == ServiceEvent.REGISTERED) {
         // font service is registered
         if (this.fontService == null) {
            this.currentRef = e.getServiceReference();
            this.fontService = (FontService)
               context.getService(currentRef);
            printReg =
						context.registerService(PRINT_SERVICE_NAME,
						new PrintServiceImpl(), null);
        }
      }
   }

   synchronized void init() {
      String fontServiceName = FontService.class.getName();
      try {
         context.addServiceListener(this,
            "(objectClass=" + fontServiceName +")");
      } catch (InvalidSyntaxException e) {
         // won't happen
      }
      ServiceReference ref =
         context.getServiceReference(fontServiceName);
      if (ref != null) {
         currentRef = ref;
         printReg = context.registerService(PRINT_SERVICE_NAME,
						new PrintServiceImpl(), null);
      }
   }
}

If you take this approach, you should know its domino effect. Assume we have bundles A, B, C, and D, intending to register services AService, BService, CService, and DService, respectively. AService needs BService, which needs CService, which in turn needs DService. And further assume that A, B, and C all adopt the cascading registration approach.

Now if D is not started, bundles A, B, and C can still be activated, but none of them will register any services. As soon as D is started and registers DService, a mini whirlwind of service registrations takes place as it triggers CService, BService, and AService to be automatically registered by their respective bundles.

Similarly, the deactivation of bundle D also causes the services from the other three bundles to be unregistered. This could be very disruptive to the overall operations of the gateway, and should be considered carefully.

This approach may also result in a deadlock if circular service dependency exists among bundles. For example, assume bundle A registers AService but needs BService in bundle B, which in turn registers BService but needs AService. No matter how you activate bundles A or B, their services will never get registered: Bundle A is waiting for the service registration event of BService, which never comes, because bundle B is waiting for the service registration event of AService. Notice this is a “benign” deadlock, because no thread is being held up, and the framework and unrelated bundles still function normally.

5.4.6. Refusing Service

In the previous examples we have shown that the calling bundle is responsible for preparing for the possibility that the service it needs may go away. In this section we demonstrate that service-providing bundles also have options of warning the callers when their services (the callees) are unregistered. If so, they can refuse to honor any call to their methods:

package com.acme.impl.print;
import org.osgi.framework.*;
import com.acme.service.print.PrintService;
public class Activator implements BundleActivator {
   static final String PRINT_SERVICE_NAME =
      "com.acme.service.print.PrintService";
   private ServiceRegistration printReg;
   private PrintService printService;

   public void start(BundleContext ctxt) {
      printService = new PrintServiceImpl(ctxt);
      printReg = ctxt.registerService(PRINT_SERVICE_NAME,
         printService, null);
   }

   public void stop(BundleContext ctxt) {
      // give client bundles a chance to release their
      // use of the print service
      printReg.unregister();
      // invalidate this instance of the print service
      ((PrintServiceImpl) printService).invalidate();
   }
}

The following is the new implementation of PrintService:

// imports
class PrintServiceImpl implements PrintService {
   private boolean amIRegistered;
   private BundleContext context;

   PrintServiceImpl(BundleContext ctxt) {
      this.context = ctxt;
      amIRegistered = true;
   }

   public void print(String printer, InputStream src)
      throws IOException
   {
      if (! amIRegistered)
						throw new IllegalStateException("Cannot print because " +
						"the service has been unregistered");
       ...
   }
   public String getStatus(String printer)
      throws IOException
   {
      if (! amIRegistered)
						throw new IllegalStateException("Cannot get status " +
						"because this service has been unregistered");
       ...
   }

   void invalidate() {
						amIRegistered = false;
						}
}

In this example, the service object has an internal flag indicating whether it itself is registered. When its bundle unregisters the service, it invalidates itself by setting the flag accordingly. Notice that we explicitly unregister the print service to give client bundles an opportunity to stop using it. If this step is omitted, the framework belatedly broadcasts the ServiceEvent.UNREGISTERING event after we have invalidated the print service, which could cause the calling bundles to fail unexpectedly.

This is a defensive mechanism for the callee, and may very well produce a harmful effect on the callers, because usually callers are not prepared to catch an instance of RuntimeException when they invoke a method on a service. There are three workarounds:

  1. Declare a checked exception to indicate that the service object is stale, so that callers have to handle it.

  2. Throw one of the existing checked exceptions if appropriate. In the previous example, the print service may just throw IOException in both methods when amIRegistered is found to be false, because from the client bundle's point of view, it appears as if the printer has been disconnected. The client bundle can then adopt the strategy of releasing the print service after a certain number of unsuccessful tries, and can get another from the service registry.

  3. Make the calling bundles implement one of the mechanisms suggested prior to this section for dealing with service unregistration.

Despite this disadvantage, the refusal of service approach predictably denies execution of methods on a unregistered service object, and may be valuable in preventing random outcome and protecting the correctness of certain application logic for some services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.172.93