Friday, October 23, 2009

Extender Pattern vs. Lifecycle Interceptor

With this post I'd like to give a little more background on the well known OSGi Extender Pattern, when this pattern is applicable and what might be an alternative when it is not applicable. There will be coverage of
  • Basics of extending an OSGi Bundle
  • Advantages and drawbacks of Extenders
  • Notion of "self ordering" LifecycleInterceptor
Extending an OSGi Bundle

The basic functional sequence of an OSGi Extender goes like this
  1. Extender registers itself as BundleListener
  2. Bundle gets installed/started
  3. Framework fires a BundleEvent
  4. Extender picks up the BundleEvent (e.g. STARTING)
  5. Extender reads metadata from the Bundle and does its work
There is no extender specific API. It is a pattern rather than a piece of functionality provided by the Framework. Typical examples of extenders are the Blueprint or Web Application Extender.

The following illustration shows how a WAR Extender might process an OSGi Web Application to register servlets that are declared in WEB-INF/web.xml with the OSGi HttpService.




Client code that installs, starts and uses the registered endpoint could look like this

// Install and start the Web Application bundle
Bundle bundle = context.installBundle("mywebapp.war");
bundle.start();

// Access the Web Application
String response = getHttpResponse("http://localhost:8090/mywebapp/foo");
assertEquals("ok", response);


This seemingly trivial code snippet has a number of issues that are probably worth looking into in more detail
  • The WAR might have missing or invalid web metadata (i.e. an invalid WEB-INF/web.xml descriptor)
  • The WAR Extender might not be present in the system
  • There might be multiple WAR Extenders present in the system
  • Code assumes that the endpoint is available on return of bundle.start()
It should be noted that the Extender, which is essentially a synchronous or asynchronous BundleListener has no way to veto the install or start transition of the bundle. Even if the Extender cannot process the metadata or fails for some other reason, the bundle will start just the same. Client code would see an ACTIVE bundle, which in fact might not be active at all in the colloquial sense of the word.

Most Blueprint or WebApp bundles are not useful if their Blueprint/Web metadata is not processed. Even if they are processed but in the "wrong" order a user might see unexpected results (i.e. the webapp processes the first request before the underlying Blueprint app is wired together).

As a consequence I would say that the extender pattern is useful in some cases but not all. IMHO it is mainly useful if a bundle can optionally be extended in the true sense of the word.

There is an RFP in the works that addresses many of the above issues. Adrian Colyer from SpringSource has initiated the work around RFP-118 Multiple Extenders in Nov-2008. The document is currently in the non-public review phase of the OSGi Alliance Enterprise Expert Group. (EEG)

Intercepting the Bundle Lifecycle

If the use case requires the notion of "interceptor" the extender pattern is less useful. The use case might be such that you would want to intercept the bundle lifecycle at various phases to do mandatory metadata processing.

An interceptor could be used for annotation processing, byte code weaving, and other non-optional/optional metadata processing steps. Traditionally interceptors have a relative order, can communicate with each other, veto progress, etc.

Lets look at how multiple interceptors can be used to create Web metadata and publish endpoints on the HttpService based on that metadata.



Here is how it works
  1. The Wep Application processor registers two LifecycleInterceptors with the LifecycleInterceptorService
  2. The Parser interceptor declares no required input and WebApp metadata as produced output
  3. The Publisher interceptor declares WebApp metadata as required input
  4. The LifecycleInterceptorService reorders all registered interceptors according to their input/output requirements and relative order
  5. The WAR Bundle gets installed and started
  6. The Framework calls the LifecycleInterceptorService prior to the actual state change
  7. The LifecycleInterceptorService calls each interceptor in the chain
  8. The Parser interceptor processes WEB-INF/web.xml in the invoke(int state, InvocationContext context) method and attaches WebApp metadata to the InvocationContext
  9. The Publisher interceptor is only called when the InvocationContext has WebApp metadata attached. If so, it publishes the endpoint from the WebApp metadata
  10. If no interceptor throws an Exception the Framework changes the Bundle state and fires the BundleEvent.
Client code is identical to above.

// Install and start the Web Application bundle
Bundle bundle = context.installBundle("mywebapp.war");
bundle.start();

// Access the Web Application
String response = getHttpResponse("http://localhost:8090/mywebapp/foo");
assertEquals("ok", response);


The behaviour of that code however, is not only different but also provides a more natural user experience.
  • Bundle.start() fails if WEB-INF/web.xml is invalid
  • An interceptor could fail if web.xml is not present
  • The Publisher interceptor could fail if the HttpService is not present
  • Multiple Parser interceptors would work mutually exclusiv on the presents of attached WebApp metadata
  • The endpoint is guaranteed to be available when Bundle.start() returns
The general idea is that each interceptor takes care of a particular aspect of processing during state changes. In the example above WebApp metadata might get provided by an interceptor that scans annotations or by another one that generates the metadata in memory. The Publisher interceptor would not know nor care who attached the WebApp metadata object, its task is to consume the WebApp metadata and publish endpoints from it.

The current BundleContext API does not allow for arbitrary attachments as contextual information to be passed between interceptors. The InvocationContext adds that plus the notion of a VirtualFile that represents the Bundle's content. The VirtualFile is necessary to access arbitrarily deeply nested resources regardless of their actual location.

Here a sneak preview of the ParserInterceptor that will come as part of the Interceptor example in JBoss OSGi 1.0.0.Beta5

public void invoke(int state, InvocationContext context)
{
  // Do nothing if the metadata is already available  
  HttpMetadata metadata = context.getAttachment(HttpMetadata.class);
  if (metadata != null)
    return;

  // Parse and create metadta on STARTING
  if (state == Bundle.STARTING)
  {
    try
    {
      VirtualFile root = context.getRoot();
      VirtualFile propsFile = root.getChild("/metadata.properties");
      if (propsFile != null)
      {
        log.info("Create and attach HttpMetadata");
        metadata = createHttpMetadata(propsFile);
        context.addAttachment(HttpMetadata.class, metadata);
      }
    }
    catch (IOException ex)
    {
      throw new LifecycleInterceptorException(ex);
    }
  }
}


and here the associate PublisherInterceptor

public void invoke(int state, InvocationContext context)
{
  // HttpMetadata is guaratied to be available because we registered
  // this type as required input
  HttpMetadata metadata = context.getAttachment(HttpMetadata.class);

  // Register HttpMetadata on STARTING 
  if (state == Bundle.STARTING)
  {
    String servletName = metadata.getServletName();
    try
    {
      log.info("Publish HttpMetadata: " + metadata);

      // Load the endpoint servlet from the bundle
      Bundle bundle = context.getBundle();
      Class servletClass = bundle.loadClass(servletName);
      HttpServlet servlet = (HttpServlet)servletClass.newInstance();

      // Register the servlet with the HttpService
      HttpService httpService = getHttpService(context, true);
      httpService.registerServlet("/servlet", servlet, null, null);
    }
    catch (RuntimeException rte)
    {
      throw rte;
    }
    catch (Exception ex)
    {
      throw new LifecycleInterceptorException(ex);
    }
  }

  // Unregister the endpoint on STOPPING 
  else if (state == Bundle.STOPPING)
  {
    log.info("Unpublish HttpMetadata: " + metadata);
    HttpService httpService = getHttpService(context, false);
    if (httpService != null)
      httpService.unregister("/servlet");
  }
}


This post and the initial implementation that is based on these concepts is a follow up of a discussion that I had with Hal Hildebrand from Oracle at one of our recent OSGi EEG meetings. Please stay tuned to find out in what shape or form these ideas develop.

In parallel to the standards efforts you will of course get this functionality as part of the next JBoss OSGi release, which should come out 01-Dec-2009.

That's it for now. In my next post I will explore the Framework launch API in more detail and show how you can bootstrap an OSGi Framework independent of the actual Framework implementation.

May this be useful.

4 comments:

  1. > Hal Hildebrand from IBM

    Uh, Hal works for Oracle last time I checked :-)

    ReplyDelete
  2. As far as integration with the MC Framework goes, you can add interceptors as part of the bootstrap config like this :


    Interceptors can of course also be added dynamically via the related LifecycleInterceptorService. In case of of the MC Framework the interceptor attachment API delegates to the DeploymentUnit, so that interceptor attachments are visible by the deployers and vice versa.

    ruandischer tee

    ReplyDelete
  3. You have a really informative website. pls continue sharing. ;)
    Web business model

    ReplyDelete