Case Study #1: Application Startup / Shutdown |
- Overview
- Module panorama.framework.startup
- Other Modules
- Application Startup
- Handling Shutdown
- Summary
The Panorama product is a fairly large J2EE web application deployed into BEA WebLogic. Panorama consists of well over six thousand classes, divided into a large number of tools and services. Panorama has been a production project for several years, long before HiveMind was available. HiveMind's introduction into Panorama (on something of a trial basis) was to cleanup the startup and shutdown process for the application.
Panorama runs inside BEA WebLogic as an enterprise application; however, it is still logically a number of subsystems, many of which require some form of startup or shutdown logic. For example, the Panorama Help service caches help data stored in the database; the Panorama Mail tool sets up periodic database cleanup jobs. All told, there are over 40 startup tasks, and a handful of shutdown tasks.
Prior to HiveMind, a single EJB was the focus of all this startup and shutdown activity. A small WebLogic startup class would invoke the EJB, and the EJB implementation would invoke static methods on many other classes (some of which would lookup other EJBs and invoke methods on them). This approach had grown quite unwieldy, especially in light of efforts to improve and modularize the Panorama build process. HiveMind was brought in to rationalize this aspect of Panorama, with the goal being to make the fewest possible changes to existing code.
An important aspect of startup and shutdown is the order of operations; there are dependencies between different tasks that must be honored in terms of which task is executed first.
Overview
The appropriate place to build the registry for an EAR is from the web application; it has the widest view of available classes; the web application classloader has visibility to the web application and its libraries, all the EJBs deployed in the application, and the system classloader.
The overall approach is to provide HiveMind module deployment descriptors for the various tools and services of Panorama; each module contributes tasks to a Startup or Shutdown configuration point.
A WebLogic shutdown class is still used and the original EJB still exists to allow an orderly shutdown. Ultimately, this is required due to class loader issues; the EJB will have visibility to the HiveMind library, but the startup class may not.
Module panorama.framework.startup
The panorama.framework.startup ("initialization and shutdown") module contains the services and configuration points for startup and shutdown. It also contains Java classes corresponding to task contributions.
module (id=panorama.framework.startup version="1.0.0") { description { "Module for startup and shutdown code within Panorama." } schema (id=Task) { element (name=task) { description { "A task which may be executed." } attribute (name=order required=true) { description { "Numeric value used to set the order of execution for tasks." } } attribute (name=title required=true) { description { "Title displayed as task is executed." } } attribute (name=class translator=object) { description { "Name of class implementing the Executable interface." } } attribute (name=service-id translator=service) { description { "Name of service implementing the Executable interface." } } conversion (class=com.panorama.framework.startup.service.Task) { map (attribute=class property=executable) map (attribute=service-id property=executable) // Other attribute map directly } // Nested element element (name=invoke-static) { description { "Used to invoke a public static method of a class." } attribute (name=class required=true) { description { "The name of the class containing the method to invoke." } } attribute (name=method) { description { "The name of the method to invoke. The default method name is " "init." } } conversion (class=com.panorama.framework.startup.service.StaticTask parent-method=setExecutable) { map (attribute=class property=className) map (attribute=method property=methodName) } } } } configuration-point (id=startup schema-id=Task) { description { "Defines startup tasks." } } configuration-point (id=Shutdown schema-id=Task) { description { "Defines shutdown tasks." } } contribution (configuration-id=Startup) { task (title=Python order=50 class=com.panorama.framework.startup.common.PythonStartup) } contribution (configuration-id=Shutdown) { task (title="Update Status" order=100) { invoke-static (class=com.panorama.framework.startup.common.PanoramaStatus method=shutdown) } } service-point (id=Startup interface=java.lang.Runnable) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=com.panorama.framework.startup.service.TaskExecutor log-property=log messages-property=messages) { set-configuration (property=tasks configuraton-id=Startup) set (property=kind value="%startup") } } interceptor (service-id=hivemind.LoggingInterceptor) } service-point (id=Shutdown interface=java.lang.Runnable) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=com.panorama.framework.startup.service.TaskExecutor log-property=log messages-property=messages) { set-configuration (property=tasks configuraton-id=Shutdown) set (property=kind value="%shutdown") } } interceptor (service-id=hivemind.LoggingInterceptor) } }
Notes:
- Extension points, configurations, schemas and services can be specified in any order.
- We use the simplest possible interface for the Startup and Shutdown services: java.lang.Runnable.
Startup configuration point
The Startup configuration point and the Startup service are closely bound together; the former contains contributions from all sorts of modules. The service uses those contributions and executes tasks based on them.
The schema for the Startup configuration point allows a < task> to be contributed. A task always has an order attribute (used to sort all the contributed elements into an execution order) and a title attribute (used in output).
The task to execute is specified in one of three ways:
- As a Java class implementing the com.panorama.framework.startup.service.Executable interface (using the class attribute)
- As a HiveMind service, implementing the service (using the service-id attribute)
- As a public static method of a class (using the enclosed < invoke-static> element)
The Executable interface is similar to the java.lang.Runnable interface:
package com.panorama.framework.startup.service; /** * Variation of <code>java.lang.Runnable</code> that allows for * the invoked method to throw an exception. */ public interface Executable { /** * Invoked to execute some kind of behavior and possible throw an exception. * The caller is responsible for catching and reporting the exception. */ public void execute() throws Exception; }
Adding throws Exception to the method signature allows the caller to be responsible for exception reporting, which simplifies the task implementations. Shortly, we'll see how the application's master servlet invokes the Startup service.
The Shutdown configuration point and service are effectively clones of the Startup configuration point and schema.
Task class
The Task class is used to hold the information collected by the Startup configuration point.
package com.panorama.framework.startup.service; import org.apache.hivemind.Orderable; /** * Configuration element for the <code>panorama.framework.startup.Startup</code> or * <code>panorama.framework.startup.Shutdown</code> * configuration points. Each element has a title, * an {@link com.panorama.framework.startup.service.Executable} * object, and an order * (used to sort the Tasks into an order of execution). */ public class Task implements Orderable, Executable { private int _order; private String _title; private Executable _executable; public void execute() throws Exception { _executable.execute(); } public int getOrder() { return _order; } public String getTitle() { return _title; } public void setOrder(int i) { _order = i; } public void setTitle(String string) { _title = string; } public Executable getExecutable() { return _executable; } public void setExecutable(Executable executable) { _executable = executable; } }
Task implements Executable, simply delegating to its executable property. In addition, it implements Orderable, which simply defines the order property (but simplifies sorting of the elements).
Startup service
The Startup and Shutdown services are very similar: similar enough that a single class, properly configured, can be the service implementation for either service.
package com.panorama.framework.startup.service; import java.util.List; import org.apache.hivemind.HiveMind; import org.apache.hivemind.Messages; import org.apache.commons.logging.Log; /** * Implementation for the <code>panorama.framework.startup.Startup</code> * and <code>Shutdown</code> services. * Reads the corresponding configuration, sorts the elements, * and executes each. */ public class TaskExecutor implements Runnable { private Log _log; private Messages _messages; private List _tasks; private String _kind; public void run() { long startTime = System.currentTimeMillis(); List sorted = null; try { sorted = HiveMind.sortOrderables(_tasks); } catch (Exception ex) { _log.error(_messages.format("initialization-failure", _kind, ex.getMessage())); return; } int count = sorted.size(); int failureCount = 0; for (int i = 0; i < count; i++) { Task task = (Task)sorted.get(i); if (execute(task)) failureCount++; } Long elapsedTime = new Long(System.currentTimeMillis() - startTime); if (failureCount > 0) _log.warn( _messages.format( "task-failure-summary", new Object[] { Integer.toString(failureCount), Integer.toString(count), _kind, elapsedTime })); else _log.info( _messages.format("task-summary", Integer.toString(count), _kind, elapsedTime)); } /** * Executes a single task. * @param task the task to execute. * @return true if the task fails (throws an exception). */ private boolean execute(Task task) { if (_log.isInfoEnabled()) _log.info(_messages.format("executing-task", _kind, task.getTitle())); try { task.execute(); return false; } catch (Exception ex) { _log.error(_messages.format("task-failure", _kind, task.getTitle(), ex.getMessage())); return true; } } public void setKind(String string) { _kind = string; } public void setLog(Log log) { _log = log; } public void setMessages(Messages messages) { _messages = messages; } public void setTasks(List list) { _tasks = list; } }
HiveMind has a static convienience method, sortOrderables() , used to sort a list of Orderable objects into order, which is used here. Remember that the contributions to the Startup (and Shutdown) configuration points are made from multiple modules and there's no way to predict in what order those contributions will show up in the tasks property, which is why explicit sorting is necessary.
At one time, there was a discussion about using a thread pool to allow execution of some of the tasks in parallel. That's a premature optimization: even with over forty startup tasks, startup still only takes about forty seconds.
StaticTask class
The StaticTask class allows an arbitrary public static method of a class to be treated like an Executable.
package com.panorama.framework.startup.service; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.apache.hivemind.ApplicationRuntimeException; import org.apache.hivemind.impl.BaseLocatable; import org.apache.commons.lang.StringUtils; /** * Implementation of * {@link com.panorama.framework.startup.service.Executable} * that Invokes a static method on a public class. */ public class StaticTask extends BaseLocatable implements Executable { private String _className; private String _methodName = "init"; public void execute() throws Exception { checkNull("className", _className); checkNull("methodName", _methodName); Class clazz = Class.forName(_className); Method m = clazz.getMethod(_methodName, null); try { m.invoke(null, null); } catch (InvocationTargetException ex) { Throwable t = ex.getTargetException(); if (t instanceof Exception) throw (Exception)t; throw ex; } } private void checkNull(String propertyName, String value) { if (StringUtils.isBlank(value)) throw new ApplicationRuntimeException( "Property " + propertyName + " of " + this + " is null.", getLocation(), null); } public String getClassName() { return _className; } public String getMethodName() { return _methodName; } /** * Sets the name of a class containing a static method that will be executed. */ public void setClassName(String string) { _className = string; } /** * Sets the name of a public static method taking no parameters. The default is "init". * */ public void setMethodName(String string) { _methodName = string; } }
The class implements Locatable , which is used in method isNull() when reporting errors; the location will be the location of the <invoke-static> element the StaticTask instance was created from.
Other Modules
Other modules, in their HiveMind module deployment descriptors, make contributions into the Startup and Shutdown configuration points of the panorama.framework.startup module. For example:
module (id=panorama.coreservice.mail version="1.0.0") { contribution (configuration-id=panorama.framework.startup.Startup) { task (title=Mail order=2600 class=com.panorama.coreservice.mail.startup.MailStartup) } }
Here, the Mail service contributes an instance of class MailStartup. Other modules take advantage of the < invoke-static> element:
module (id=panorama.coreservice.garbagecollection version="1.0.0") { contribution (configuration-id=panorama.framework.startup.Startup) { task (title="Scheduling Garbage Collection" order=3900) { invoke-static (class=com.panorama.coreservice.garbagecollection.startup.GarbageCollectionStartup) } } }
Application Startup
The master servlet for the web application is responsible for constructing the registry and storing it so that other code may access it.
public void init() throws ServletException { LOG.info("*** Bootstrapping HiveMind Registry ***"); if (PanoramaRuntime.getHiveMindRegistry() != null) { LOG.info( "Registry is already initialized (the application appears to have been redeployed)."); return; } try { RegistryBuilder builder = new RegistryBuilder(new RegistryBuilderErrorHandler()); ClassResolver resolver = new DefaultClassResolver(); builder.processModules(resolver); Registry registry = builder.constructRegistry(Locale.getDefault()); PanoramaRuntime.setHiveMindRegistry(registry); Runnable startup = (Runnable)registry.getService("panorama.framework.startup.Startup", Runnable.class); LOG.info("*** Executing panorama.framework.startup.Startup service ***"); startup.run(); } catch (Exception ex) { LOG.error( "Unable to execute panorama.framework.startup.Startup service: " + ex.getMessage()); } }
After building the registry, the servlet uses the Startup service to indirectly execute all the startup tasks.
Handling Shutdown
We take advantage of a WebLogic extension to know when the application server is being shut down.
package com.panorama.framework.startup; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import com.panorama.framework.startup.ejb.Shutdown; import com.panorama.framework.startup.ejb.ShutdownHome; /** * Shutdown class called by the WebLogic container. */ public class Shutdown { private final static String EJB_JNDI_NAME = "com.panorama.framework.startup.ejb.initshutHome"; /** Prevent instantiation */ private Shutdown() { } /** * Gets the Shutdown EJB and invokes <code>shutdown()</code>. */ public static void main(String args[]) throws Exception { InitialContext context = new InitialContext(); ShutdownHome home = (ShutdownHome)PortableRemoteObject.narrow( context.lookup(EJB_JNDI_NAME), ShutdownHome.class); Shutdown bean = (Shutdown)home.create(); bean.shutdown(); } }
The implementation of the initshut EJB is similarily straight-forward:
package com.panorama.framework.startup.ejb; import java.rmi.RemoteException; import javax.ejb.CreateException; import org.apache.hivemind.HiveMind; import org.apache.hivemind.Registry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.panorama.framework.ejb.BaseSessionBean; /** * Handles shutdown logic. * */ public class ShutdownEJB extends BaseSessionBean { private static final Log LOG = LogFactory.getLog(ShutdownEJB.class); public void ejbCreate() throws RemoteException, CreateException { } /** * Called by J2EE Container shutdown class for Panorama shutdown processing. * * <p> * Gets the <code>panorama.framework.startup.Shutdown</code> service and executes it. * */ public void shutdown() throws RemoteException { Registry registry = PanoramaRuntime.getHiveMindRegistry(); if (registry == null) { LOG.error( "No HiveMind module registry is in place, unable to execute an orderly shutdown."); return; } Runnable r = (Runnable)registry.getService("panorama.framework.startup.Shutdown", Runnable.class); r.run(); LOG.info("**** Panorama shutdown complete ****"); } }
Summary
This case study has shown how easy it is to leverage HiveMind for a complex task. A monolithic EJB was broken down into tiny, agile contributions to a configuration point. The startup and shutdown logic is kept close to the contributing modules, in those modules' HiveMind deployment descriptors. Contributions are in expressive, easily readable XML.
A single class is used to implement multiple, similar services, just by configuring it as needed. Links between different aspects of the system (such as the servlet initialization code and the Startup service) are kept simple and agile.
The small amount of code necessary to orchestrate all this is fully tested in a unit test suite.
The end result: an agile, easily extended system. HiveMind has provided the tools and environment to support an elegant, data-driven solution ... replacing the old, code-heavy EJB implementation.