CORBA middleware has been used successfully in domains ranging from telecommunications to aerospace, process automation, and e-commerce to enable developers to create applications rapidly that can meet a particular set of requirements with a reasonable amount of effort. Historically, however, many CORBA solutions have tightly coupled interfaces and implementations, which make it hard to adapt to requirement or environment changes that occur late in an application's life cycle (i.e., during deployment and/or at run time). To address this problem, CORBA supports metaprogramming mechanisms [1] that improve the adaptability of distributed applications by allowing their behavior to be modified with little or no change to existing application software.
This column describes CORBA Portable Interceptors, which are objects that an ORB invokes in the path of an operation invocation to monitor or modify the behavior of the invocation transparently. Portable Interceptors are a metaprogramming mechanism that implements the Interceptor pattern [2, 3], which allows applications to extend and control the behavior of a framework. Since the CORBA Portable Interceptor specification is fairly large, we focus on concepts and components in this column and defer our C++ programming examples until the next column. Subsequent columns will then discuss other CORBA metaprogramming mechanisms, such as extensible transports and smart proxies.
Overview of CORBA Metaprogramming Mechanisms
CORBA middleware provides stub and skeleton mechanisms that serve as a "glue" between the client and servants, respectively, and the ORB. CORBA stubs implement the Proxy pattern [4] and marshal operation information and data type parameters into a standardized request format. Likewise, CORBA skeletons implement the Adapter pattern [4] and demarshal the operation information and typed parameters stored in the standardized request format.
CORBA stubs and skeletons can be generated automatically from interfaces defined using OMG IDL. A CORBA IDL compiler transforms application-supplied OMG IDL definitions into stubs and skeletons written using a particular programming language, such as C++ or Java. In addition to providing programming language and platform transparency, an IDL compiler eliminates common sources of network programming errors and provides opportunities for automated compiler optimizations [5].
Traditionally, the stubs and skeletons generated by an IDL compiler are fixed (i.e., the code emitted by the IDL compiler is determined at translation time). This design shields application developers from the tedious and error-prone network programming details needed to transmit client operation invocations to server object implementations. Fixed stubs and skeletons make it hard, however, for applications to adapt readily to certain types of changes in requirement or environmental conditions, such as:
- The need to log information or monitor system resource utilization may not be recognized until after an application has been deployed.
- There may be a need to acquire extra debugging information at run time about a problematic application at a customer site.
- Remote operations may require additional parameters and authentication protocols to execute securely in certain environments.
- The priority at which clients invoke or servers handle a request may vary according to environmental conditions, such as the amount of CPU or network bandwidth available at run time.
- The actual host on which a remote operation is executed may need to change at run time depending on system workload and resource availability.
In applications based on CORBA middleware with conventional fixed stubs/skeletons, these types of changes often require reengineering and restructuring of existing application software. One way to minimize the impact of these changes is to employ metaprogramming mechanisms that allow applications to adapt to various types of changes with little or no modifications to existing software. For example, stubs, skeletons, and certain other points in the end-to-end operation invocation path can be treated as meta-objects [1], which are objects that refine the capability of base-level objects that comprise the bulk of application programs.
As shown in Figure 1, CORBA ORBs are responsible for transmitting client operation invocations to target objects. When a client invokes an operation, a stub implemented as a meta-object can act in conjunction with transport-protocol meta-objects to access and/or transform a client operation invocation into a message and transmit it to a server. Corresponding meta-objects on the server's request processing path can access and/or perform inverse transformations on the operation invocation message and dispatch the message to its servant. An invocation result is delivered in a similar fashion in the reverse direction.
As all operation invocations pass through meta-objects, certain aspects of application and middleware behavior can be adapted transparently when system requirements and environmental conditions change by simply modifying the meta-objects. To modify meta-objects, the DOC middleware can either (1) provide mechanisms for developers to install customized meta-objects for the client or (2) embed hooks implementing a MOP (meta-object protocol) [1] in the meta-objects and provide mechanisms to install objects implementing the MOP to strategize these meta-object behaviors. In the context of CORBA, there are various mechanisms that can implement the MOP:
- Smart Proxies, which are application-defined stub implementations that transparently override the default stubs created by an ORB to customize client behavior on a per-interface basis.
- Portable Interceptors, which are objects that an ORB invokes in the path of an operation invocation to monitor or modify the behavior of the invocation transparently.
- Servant Managers, which allow server applications to register objects that activate servants on demand, rather than creating all servants before listening for requests.
- Extensible Transports, which decouple the ORB's transport protocols from its component architecture so that developers can add new protocols without requiring changes to the ORB or application software.
This column focuses on Portable Interceptors; Servant Managers were described in [6]. Future columns in this series will cover Smart Proxies and Extensible Transports.
Overview of Portable Interceptors
As outlined above, CORBA Portable Interceptors are a metaprogramming mechanism that can increase the flexibility of client and server applications. Portable Interceptors are standard meta-objects that stubs, skeletons, and certain points in the end-to-end operation invocation path can invoke at predefined "interception points." Two types of interceptors are defined in the CORBA Portable Interceptor specification:
- Request interceptors, which deal with operation invocations.
- IOR interceptors, which insert information into IORs (interoperable object references).
We describe both types of interceptors in this section.
Request Interceptors
Request interceptors can be decomposed into client request interceptors and server request interceptors, which are designed to intercept the flow of a request/reply sequence through the ORB at specific points on clients and servers, respectively. Developers can install instances of these interceptors into an ORB via an IDL interface defined by the CORBA Portable Interceptor specification. Regardless of what interface or operation is invoked, after request interceptors are installed they will be called on every operation invocation at the predetermined ORB interception points shown in Figure 2.
As shown in Figure 2, request interception points occur at four points along the end-to-end invocation path from client to server. There are a total of 10 different interception hook methods that can be called at different points in this interceptor chain, including:
- When a client sends a request (e.g., the send_request() hook is called on the client before the request is marshaled).
- When a server receives a request (e.g., the receive_request() hook is called on the server after the request is demarshaled).
- When a server sends a reply (e.g., the send_reply() hook is called on the server before the reply is marshaled).
- When a client receives a reply (e.g., the receive_reply() hook is called on the client after the reply is demarshaled).
Compared to a client invocation path, a server invocation path has an additional interception point called receive_request_service_contexts(), which is invoked before the POA dispatches a servant manager. This interception point prevents unnecessary upcalls to a servant. For example, in the CORBA Security Service [7] framework this interception point can be used to inspect security-related credentials piggybacked in a service context list entry. If the credentials are valid, the upcall can proceed to other interceptors (if they exist) or to the servant; if not, an exception will be returned to the client.
Application developers can customize the behavior of their software at interception points as follows:
- Subclass from the appropriate base class, such as PortableInterceptor::ClientRequestInterceptor or PortableInterceptor::ServerRequestInterceptor, and overriding the appropriate hook method(s), such as send_request() or receive_request().
- Creating an instance of the subclass and registering it with the ORB. Interceptors are installed in the ORB via an ORBInitializer object and registered by implementing its pre_init() or post_init() method and calling PortableInterceptor::register_orb_initializer() prior to calling CORBA::ORB_init().
At run time, an interceptor can examine the state of the request that it is associated with and perform various actions based on the state. For example, interceptors can invoke other CORBA operations, access information in a request, insert/extract piggybacked messages in a request's service context list, redirect requests to other target objects, and/or throw exceptions based on the object the original request is invoked upon and the type of the operation. Each of these capabilities is described below:
- Nested invocations. A request interceptor can invoke operations on other CORBA objects before the current invocation it is intercepting completes. For example, monitoring and debugging utilities can use this feature to log information associated with each operation invocation. To avoid causing infinite recursion, developers must be careful to act only on targeting interfaces and operations they intend to affect when performing nested invocations in an interceptor.
- Accessing request information. Request interceptors can access information associated with an invocation, such as the operation name, parameters, exception lists, return values, and the request id via the MOP interface as defined in the Portable Interceptor specification. Interceptors cannot, however, modify parameters or return values. This request/reply information is encapsulated in an instance of ClientRequestInfo or ServerRequestInfo classes, which derive from the RequestInfo class and contain the information listed above for each invocation. For example, client request interceptors are passed ClientRequestInfo and server request interceptors are passed ServerRequestInfo. These RequestInfo-derived objects can use features provided by the CORBA Dynamic module, which we've covered in our recent columns on Dynamic CORBA [8, 9, 10]. This module is a combination of pseudo-IDL types, such as RequestContext and Parameter, declared in earlier CORBA specifications. These types facilitate on-demand access of request information from RequestInfo to avoid unnecessary overhead if an interceptor does not need all the information available with RequestInfo.
- Service context manipulation. As mentioned earlier, request interceptors cannot change parameters or the return value of an operation. They can, however, manipulate service contexts that are piggybacked in operation requests and replies exchanged between the clients and servers. A service context is a sequence field in a GIOP message that can transmit "out-of-band" information, such as authentication credentials, transaction contexts, operation priorities, or policies associated with requests. For example, the CORBA Security Service [7] uses request interceptors to insert user identity via service contexts. Likewise, the CORBA Transaction Service uses request interceptors to insert transaction-related information into service contexts so it can perform extra operations, such as commit/rollback, based on the operation results in a transaction. Each service context entry has a unique service context identifier that applications and CORBA components can use to extract the appropriate service context.
- Location forwarding. Request interceptors can be used to forward a request to a different location, which may or may not be known to the ORB in advance. This capability is performed via the PortableInterceptor::ForwardRequest exception, which allows an interceptor to inform the ORB that a retry should occur upon the new object indicated in the exception. The exception can also indicate whether the new object should be used for all future invocations or just for the forwarded request. Since the ForwardRequest exception can be raised at most interception points, it can be used to provide fault tolerance and load balancing [11]. For example, the IOR of a replicated object can be used as the forward object in this exception. When the object dies for some reason and this situation is conveyed to the interceptor this exception can be raised even before the POA tries to make an upcall.
- Multiple interceptors. Multiple request interceptors can be registered with an ORB, which will then iterate through them and invoke the appropriate interception operation at every interception point according to the following rules:
- For each request interceptor, only one starting interception point can be called for a given invocation. A starting interception point is the first point invoked in a request/reply sequence. For instance, the starting points for a client ORB include send_request() and send_poll(). Likewise, the starting point for a server ORB is receive_request_service_contexts().
- For each request interceptor, only one ending interception point can be called for a given invocation. The ending interception point is the last juncture where an interception may occur in the request/reply sequence. The ending interception points on a client ORB are receive_reply(), receive_exception(), and receive_other() and the ending interception points for a server ORB consist of send_reply(), send_exception(), and send_other().
- There can be multiple intermediate interception points.
- Intermediate interception points cannot be invoked in the case of an exception.
- The ending interception point for a given interceptor will be called only if the starting interception point runs to completion.
Multiple interceptors are invoked using a flow-stack model. When initiating an operation invocation, an interceptor is pushed onto the stack after its starting interception point completes successfully. When an invocation completes, the interceptors are popped off the stack and invoked in reverse order. The flow-stack model ensures that only interceptors executed successfully for an operation can process the reply/exceptions.
- Exception handling. Request interceptors can affect the outcome of a request by raising exceptions in the inbound or outbound invocation path. In such cases, the send_exception() operation of a server request interceptor is invoked on the reply path and is received at the client in the receive_exception() interceptor hook. When a send_exception() or receive_exception() operation raises a ForwardRequest exception, the other interceptors have their send_other() and receive_other() interception points invoked, respectively.
IOR Interceptors
IIOP v1.1 introduced an attribute called components, which contains a list of tagged components to be embedded within an IOR. When an IOR is created, tagged components provide a placeholder for an ORB to store extra information pertinent to the object. This information can contain various types of QoS information related to security, server thread priorities, network connections, CORBA policies, or other domain-specific information.
The original IIOP v1.0 specification provided no standard way for applications or services to add new tagged components into an IOR. Services that require this field were therefore forced to use proprietary ORB interfaces, which impeded their portability. The Portable Interceptors specification resolves this problem by defining IOR interceptors.
IOR interceptors are objects invoked by the ORB when it creates IORs. They allow an IOR to be customized (e.g., by appending tagged components). Whereas request interceptors access operation-related information via RequestInfos, IOR interceptors access IOR-related information via IORInfos. Figure 3 illustrates the behavior of IOR interceptors.
A server ORB that is responsible for creating an IOR contains an IOR interceptor repository. In turn, this repository contains a series of IOR interceptors that have been registered with the ORB. When the server process requests the ORB to create an IOR, the ORB iterates through the IOR interceptors in the repository using the establish_components() operation. The IOR interceptors then add tagged components to the IOR being generated by referring to the IORInfo passed in by calling add_ior_component() or add_ior_component_to_profile().
Concluding Remarks
CORBA shields developers from many tedious and error-prone aspects of programming distributed applications. Without proper support from the middleware, however, it can be hard to evolve distributed applications after they are deployed. This column has discussed how the concepts and components in the CORBA Portable Interceptors specification help the adaptability of distributed applications by allowing their behavior to be modified without changing existing software drastically. Interceptors can be applied to either servers or clients and can access operation-specific information. They therefore provide an effective metaprogramming mechanism to handle advanced features, such as authentication and authorization, transparently end-to-end.
This column concludes the introduction to our multi-part series on CORBA metaprogramming mechanisms. Our next column will illustrate how to program Portable Interceptors using C++. Subsequent columns will cover other CORBA metaprogramming mechanisms. As always, if you have comments, questions, or suggestions regarding CORBA or our column in general, please let us know at [email protected].
References
[1] Nanbor Wang, Douglas C. Schmidt, Ossama Othman, and Kirthika Parameswaran. "Evaluating Metaprogramming Mechanisms for ORB Middleware," IEEE Communication Magazine, October 2001.
[2] Douglas C. Schmidt, Michael Stal, Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects (Wiley and Sons, 2000).
[3] Steve Vinoski. "Toward Integration: Chain of Responsibility," IEEE Internet Computing, November/December 2002, pp. 80-83.
[4] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).
[5] Eric Eide, Kevin Frei, Bryan Ford, Jay Lepreau, and Gary Lindstrom. "Flick: A Flexible, Optimizing IDL Compiler," Proceedings of ACM SIGPLAN '97 Conference on Programming Language Design and Implementation (PLDI), Las Vegas, NV, June 1997.
[6] Douglas C. Schmidt and Steve Vinoski. "C++ Servant Managers for the Portable Object Adapter," C++ Report, September 1998.
[7] Object Management Group. "Security Service Specification," OMG Document formal/02-03-11version 1.8, March 2002.
[8] Steve Vinoski and Douglas C. Schmidt. "Object Interconnections: Dynamic CORBA: Part 1, The Dynamic Invocation Interface," C/C++ Users Journal C++ Experts Forum, July 2002, <www.cuj.com/experts/2007/vinoski.htm>.
[9] Steve Vinoski and Douglas C. Schmidt. "Object Interconnections: Dynamic CORBA: Part 2, Dynamic Any," C/C++ Users Journal C++ Experts Forum, September 2002, <www.cuj.com/experts/2009/vinoski.htm>.
[10] Steve Vinoski and Douglas C. Schmidt. "Object Interconnections: Dynamic CORBA: Part 3, The Dynamic Skeleton Interface," C/C++ Users Journal C++ Experts Forum, November 2002, <www.cuj.com/experts/2011/vinoski.htm>.
[11] Ossama Othman, Carlos O'Ryan, and Douglas C. Schmidt. "An Efficient Adaptive Load Balancing Service for CORBA," IEEE Distributed Systems Online, March 2001.
[12] Object Management Group. "The Common Object Request Broker: Architecture and Specification Revision 3.0," OMG Technical Document formal/XYZ02-06-33, July 2002.
[13] Michi Henning and Steve Vinoski. Advanced CORBA Programming with C++ (Addison-Wesley, 1999).
About the Author
Steve Vinoski is vice president of Platform Technologies and chief architect for IONA Technologies and is also an IONA Fellow. A frequent speaker at technical conferences, he has been giving CORBA tutorials around the globe since 1993. Steve helped put together several important OMG specifications, including CORBA 1.2, 2.0, 2.2, and 2.3; the OMG IDL C++ Language Mapping; the ORB Portability Specification; and the Objects By Value Specification. In 1996, he was a charter member of the OMG Architecture Board. He is currently the chair of the OMG IDL C++ Mapping Revision Task Force. He and Michi Henning are the authors of Advanced CORBA Programming with C++, published in January 1999 by Addison Wesley Longman. Steve also represents IONA in the W3C (World Wide Web Consortium) Web Services Architecture Working Group.
Doug Schmidt is a full professor at Vanderbilt University. His research focuses on patterns, optimization principles, model-based software development, and empirical analyses of object-oriented techniques that facilitate the development of high-performance, real-time distributed object computing middleware on parallel processing platforms running over high-speed networks and embedded-system interconnects. He is the lead author of the books Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, published by Wiley and Sons, and C++ Network Programming: Mastering Complexity with ACE and Patterns and C++ Network Programming: Systematic Reuse with ACE and Frameworks both published by Addison-Wesley. He can be contacted at [email protected].