As explained in Section 3.3, component configurators are responsible for holding information about the dynamic inter-component dependencies at runtime and implementing reconfiguration policies.
We begin this chapter by stressing the benefits of this model in achieving a clear separation of concerns. Next, we describe concrete implementations of component configurators in C++, Java, and CORBA. Finally, we discuss customization of component configurators.
By combining the ideas of computational reflection [Smi84] and object-orientation, Kiczales created the concept of the meta-object protocol [KdRB91]. In his model, the programming language can be modified and extended by the user, who becomes a partner in language design. The protocol makes a distinction between base-level and meta-level objects. It uses meta-level objects, or meta-objects, to specify the behavior of the basic language operations and constructs. If the user does not alter the meta-level, the default meta-objects are used and the language is not altered. However, if the user specializes the meta-objects by using object-oriented inheritance, then the language behavior changes and can be customized to the application.
The meta-object protocol allows for a clean separation of concerns in the implementation of non-functional aspects of base-level objects. Aspects such as persistence, replication, and reconfiguration can be addressed by the meta-objects while the basic application objects are not modified.
Although this is an extremely flexible model that allows for a clean separation of concerns, previous implementations led to significant performance degradations since every method invocation to an object was intercepted by its meta-object.
Kramer introduced the concept of configuration programming [Kra90], claiming that, to construct and manage distributed applications in a clean and effective way, it is necessary to adopt a configuration language independent of the component implementation language. He argued that the code for configuration should be separated from the component code.
Instead of relying on a different configuration language such as Darwin [MDK94,MTK97], this thesis proposes a novel model in which the basic functionality of the configuration engine is exported through a well-defined interface5.1. The actual configuration code is written in any language the developer chooses and is encapsulated in the component configurators. The latter can be reused in different contexts, but they can also be customized to each situation.
Using reflection terminology, component configurators are meta-objects, but with the advantage that their code is only executed when (re)configuration is required. They do not intercept every method call and, therefore, do not degrade application performance.
Our model is better suited for the highly heterogeneous environments of the future since the configuration engine can be defined using OMG IDL and then accessed, via CORBA, from any platform, using any programming language. Besides, the dynamic dependencies are accessible at runtime and, unlike in conventional configuration languages, can be inspected and modified at any time.
Component configurators are normally dormant until they receive a request for reconfiguration or some other event notification. However, if the application developer desires, a certain component configurator can be active and have its own thread of execution. In this case, it could, for example, monitor system activity and send requests and event notifications to other configurators and components.
Depending on the way it is implemented, a component configurator is capable of referring to components running on a single address space, on different address spaces and processes, or even running on different machines in a distributed system.
First, we describe a C++ implementation for a single address space. Next, we describe a Java implementation that was initially designed for a single address space but later extended to distributed systems using Java RMI. Finally, we describe a CORBA implementation for distributed systems.
Figure 5.1 contains a simplified declaration5.2 of a ComponentConfigurator base class in C++. Figure 5.2 shows a schematic representation of some of its method calls.
The class constructor receives a pointer to the component implementation as a parameter. It can be later obtained through the implementation() method.
The addHook() and deleteHook() methods are used to create and remove component configurator hooks. Each hook is identified by a string, representing its name.
The hook() method is used to specify that this component depends upon another component and unhook() breaks this dependence. The registerClient() and unregisterClient() methods are similar to hook() and unhook() but they specify that other components (called clients) depend upon this component.
eventFromHookedComponent() announces that a component which is attached to this component has sent an event. The ComponentConfigurator() class is subclassed to implement different behaviors when events are reported. Examples of common events are the destruction of a hooked component, the internal reconfiguration of a hooked component, or the replacement of the implementation of a hooked component.
eventFromClient() is similar to the previous method but it announces that a client has sent an event. This can be used, for example, to trigger reconfigurations in a component to adapt to new conditions in its clients. Our reference implementation defines a basic set of events including STARTED, FINISHED, SHUTDOWN, RECOVERED, RECONFIGURED, REPLACED, MIGRATED, DELETED, and FAILED. Applications can extend this set by defining their own events.
name() returns a pointer to a string containing the name of the component and info() returns a pointer to a string containing a description of the component. Specific info() implementations can return different kinds of information like a list of configuration options accepted by the component, or a URL for its documentation and source code.
listHooks() returns a pointer to a list of DependencySpecifications. A DependencySpecification is a structure defined as
struct DependencySpecification {
const char *hookName;
ComponentConfigurator *component;
};
listClients() returns a pointer to a list of DependencySpecifications corresponding to the components that depend on this component (its clients) and the name of the hooks (in the client's ComponentConfigurator) to which this component is attached.
Finally, getHookedComponent() returns a pointer to the configurator of the component that is attached to a given hook.
The Java implementation is very similar to the C++ one. Its declaration as a Java interface is shown in Figure 5.3.
There are two minor differences with respect to the C++ version. The first is the destroyComponentConfigurator() method. Unlike C++, Java does not have explicit destructors. So, instead of calling the delete operator, as done in C++, Java programmers can request a component configurator to release its resources and prepare for its destruction by calling the destroyComponentConfigurator() method. Concrete implementations of the ComponentConfigurator interface can also choose to call this method on their implementation of the finalize () method, which is called by the JVM garbage collector.The second difference is the addition of exceptions. When we first implemented the C++ ComponentConfigurator, not every C++ compiler supported exceptions properly5.3. The C++ compilers that do support exceptions, generate a considerably larger code when exceptions are used, which may be undesirable for small devices with limited memory. So, we decided not to add them to the base C++ class. Java compilers and virtual machines always support exceptions, so our Java ComponentConfigurator uses them.
In CORBA, the ComponentConfigurator interface is defined using OMG IDL. Figure 5.4 shows a complete definition of the Configuration module which includes the ComponentConfigurator interface, definitions of dependencies and type structures, and a Factory interface.
In this case, the interface can be implemented in any programming language and different implementations can communicate with each other seamlessly. We developed an implementation in C++ while other students in our department (Jennifer Jackson and Steve Zelinka) developed a Java version using the ORB that comes with the Java 1.2 distribution.What is new here is the introduction of a factory for component configurators. The factory is a CORBA server that is responsible for creating component configurator objects that reside in the address-space in which the factory is located. Depending on where the programmer chooses to place the factories, the component configurators can be (1) co-located with their respective component implementations, (2) located in a separate process in the same machine or (3) located in a central node on the network while the component implementations are distributed. In all three cases, the communication with the component configurators is done exactly in the same way, via CORBA. No changes are required in the code to interact with local or remote component configurators.
In the CORBA implementation, a DependencySpecification stores a CORBA Interoperable Object Reference (IOR) so that the component configurator is able to reify dependencies among distributed components.
When a CORBA component is destroyed, the component implementation (or the ORB) must call the configurator destructor so that it can tell its clients that the destruction is taking place. If a node crashes or if the whole process containing both the component and the configurator crash, it might not be possible to execute the configurator destructor. In this case, the clients will not be informed of the component destruction. Subsequent CORBA invocations to the crashed component will raise an exception announcing that the object is not reachable or that it does not exist. This exception may be caught by the client component, which would be responsible for locating a new server component and updating its component configurator. Alternatively, the exception could be caught by an interceptor installed in the ORB, which could locate the new server and update the client configurator without the client's knowledge.
Our infrastructure includes a basic component configurator that provides simple implementations for the dependency management operations (e.g., addHook(), registerClient()) and a few subclasses implementing additional functionality such as mutual-exclusion (see Section 5.3.2).
Programmers can extend these basic classes in two ways. First, they can be customized to provide some generic functionality that could be reused by several applications in different contexts. This could include support for persistence, security, replication, and so on. In Section 5.3.4, we discuss how to customize a component configurator to support dynamic reconfiguration.
The second way of extending component configurators refers to application-specific extensions. For example, the programmer of a computationally-intensive distributed scientific application could include code to monitor local CPU load in the component configurator. The configurators, then, could exchange events with information about the CPU load on their machines and implement an algorithm for dynamic load balancing. The component would contain the code for carrying out the application task (scientific computation) while the component configurator would encapsulate the policies for load balancing.
We now discuss some examples of component configurator customization that are particularly interesting.
We present here a simple example of an application-specific customization. A detailed example of a customization to support fault-tolerance in a QoS-sensitive application is presented in Section 7.2.
The dynamic dependence information enables the reconfiguration of components that are already running. Although our infrastructure does not guarantee safe reconfiguration by itself, it does provide a valuable framework for programmers to implement safe reconfiguration more easily and uniformly.
Continuing with our web browser example (introduced in Section 4.2), the application developer could implement a WebBrowserConfigurator by using inheritance from the ComponentConfigurator class and customizing it to deal with the dynamic replacement of the system's JVM. As shown in Figure 5.5, the eventFromHookedComponent method can be overridden to catch REPLACED events coming from the JVM ComponentConfigurator.
When the implementation of the JVM is updated, the JVMConfigurator sends a REPLACED event to its clients. When the WebBrowserConfigurator receives this event, it freezes all the objects in the current JVM (using the Java object serialization mechanism), updates the current JVM with the new JVM implementation, and melts the objects in the new JVM5.4.
The most basic version of the C++ component configurator we implemented is called SimpleConfigurator. It does not provide any support for controlling concurrent accesses to the configurator by multiple threads. To support that, we implemented a subclass called LockingConfigurator.
LockingConfigurator uses two locks. The first controls read and write accesses to the dependencies and the second, to the component implementation. Both permit either multiple concurrent readers (and no writers) or a single writer.
The listHooks() method, for example, is implemented by first acquiring the dependencies lock in reading mode, then calling the equivalent method in the super class, and then releasing the lock.
inline DependencyList *listHooks ()
{
dependenciesLock_.acquire_read();
DependencyList *ret = SimpleConfigurator::listHooks();
dependenciesLock_.release ();
return ret;
}
The registerClient() method, on the other hand, acquires the lock in writing mode.
inline int registerClient (ComponentConfigurator *client, const char *hookNameInClient)
{
dependenciesLock_.acquire_write();
int ret = SimpleConfigurator::registerClient(client, hookNameInClient);
dependenciesLock_.release ();
return ret;
}
The implementationLock can be obtained through an accessor method. In that way, programmers can lock the implementation of a component and be sure that it will not change while it is locked.
After some experience with the configurators, we noticed that, in some applications, it is desirable to assign attributes to dependencies. In that way, we can have different types of dependencies. Dependency attributes can also be used to store more information about the characteristics of that dependency. Developers can program policies that treat dependencies differently based on their attributes.
Consider, for example, a File Server that has two types of clients. The first are simple client programs activated by users. The second are backup storage devices that simply read everything from the File Server from time to time, saving its contents into tape. When the File Server is shut down by the administrator, its configurator may need to send an event to the backup devices so that its content can be copied. In this case, the shut down process should be delayed until all the server contents is backed up to tape. By using attributes, the File Server configurator can differentiate between the clients that need to be notified synchronously (backup devices) and clients that do not need to be notified (user programs).
Figure 5.6 shows the Java
ComponentConfiguratorAttrib interface that inherits from the basic
ComponentConfigurator interface, adding support for attributes.
DependencyAttributes is a list of attributes; each attribute is
defined as a
name,value
pair, where name is a string and
value is any Java object.
Our implementation of the ComponentConfiguratorAttrib interface was used in a decentralized information system [ESS+00] described in Section 5.4.
In a general sense, when replacing an old component by a new one it may be necessary to transfer the state from the former to the latter. This process can be automated by the reconfiguration engine by requiring every component to implement a pair of operations get_state() and set_state(). All the components of a certain type must then agree on a common external representation for the state of the components of that type. Then, the underlying engine simply transfers the state from one component to the other, without having to interpret its meaning.
To replace a component and remove the old version safely, one must make sure that no other component will try to contact the component being removed. This can be achieved by using a combination of the following five mechanisms.
It is possible to implement these mechanisms in a specialized subclass of the ComponentConfigurator. This can be achieved with an implementation of the ideas that Bloom and Day discuss in [BD93], providing a more sophisticated support for dynamic reconfiguration at the middleware level and requiring less application participation. Different combinations of these mechanisms can be used in different parts of a single system.
The ComponentConfigurator model described in this chapter has been deployed in the following systems.
According to experimental results, the use of the component configurator for dependence management in this system imposes an overhead of 10% at initialization time. After the initialization is completed, the component configurators only act when reconfiguration is required and, therefore, impose no overhead during normal system execution [ESS+00].
In addition to the completed systems described above, our model is being deployed in the following ongoing projects.