Previous Up Next

Chapter 21  Object Transaction Service

21.1  Associating CORBA Objects with Database Records

When designing a CORBA server that interacts with a database, you might decide to have a separate CORBA object for each record in a database table. To do this, you need to associate a CORBA object with the corresponding record in the database. This is easily achieved by storing the primary key of the database record in the object id (Section 5.6.1) of the CORBA object.

You could have a separate servant (Section 5.2) for each CORBA object/database record. If you do this then you might wish to store the object id in an instance variable of the servant. However, it is unusual for servants to cache any information from the database in instance variables. This is because of the risk that the cached information would become stale if the database was ever updated directly rather than being updated via the CORBA server. Having a separate servant for each record in a database table, where the only instance variable in each servant is an object id (acting as a primary key into a database table) is wasteful of memory. A more scalable approach is to use one default servant (Section 5.6.4) to represent all the CORBA objects. The default servant can call the get_object_id() operation on the POACurrent (Chapter 13) to find out which CORBA object it is representing for the current request. The default servant then uses this object id as the primary key into a database table.

21.2  Per-operation Transactions

In many client-server systems, the body of IDL operations are implemented as shown in the pseudocode below:


void some_operation(...)
{
	    begin_transaction();
	    ... // query or update a record in the database
	    commit_transaction();
}

Note that the entire transaction is contained within the body of a single IDL operation. This is often called a per-operation transaction. If you intend to write a CORBA server whose use of transactions is restricted to per-operation transactions then how you interact with the database is completely independent of your use of CORBA. For example, you can use whatever brand of database you want, and you can interact with that database using whatever techniques you want, such as embedded SQL, Oracle OCI, ODBC or JDBC.

The use of per-operation transactions is sufficient for a great many applications. However, some client-server systems require the ability for a transaction to span multiple operation calls from a client to one server. This type of interaction requires use of the CORBA Object Transaction Service (OTS). Some other client-server interactions involve access to multiple databases. These types of transactions are usually called distributed transactions, and they also require use of OTS.

21.3  Overview of Distributed Transactions

The concept of a distributed transaction pre-dates CORBA, and is independent of any one particular kind of middleware. Let us assume that a client wishes a transaction to span queries and updates on two databases. In order to do this, the client makes use of a transaction manager (TM), as shown in Figure 21.1.

Figure 21.1: A distributed transaction
Figure 21.2: Two-phase commit

When the client wants to begin a transaction, it sends a request to the TM (step 1). The TM sends back an identifier that uniquely identifies the newly-started transaction. The client then sends its query and/or update requests to the databases or, in a middleware system like CORBA, to the server processes that wrap the databases; this is shown in steps 2 and 3. The transaction identifier is transmitted as part of these requests. Whenever a database (or a server process that wraps the database) is accessed by the client, the database (server) uses the received transaction identifier to tell the TM that it (the database/server) is taking part in the transaction (steps 2a and 3a). The TM uses a persistent storage area (for example, a file or its own database) to keep track of which databases/servers are taking part in which transactions. Finally, the client notifies the TM that it wishes to commit the transaction (step 4). To avoid too much clutter in a single diagram, the rest of the interaction steps are shown in Figure 21.2. The TM engages in a two-phase commit dialog with all the databases/servers that have taken part in the transaction.

In the first phase of the two-phase commit protocol, the TM asks each database/server to prepare to commit. Each database/server prepares itself by persisting any updates to disk, but in a way that it can undo the changes (for example, the database might persist not just changes but also details of how to undo the changes). The database/server then replies to the TM that it is voting to either commit or rollback the transaction.

In the second phase of the two-phase commit protocol, the TM analyses all the votes from the databases/servers. If all the votes are to commit then the TM instructs each database/server to commit its changes. If any of the votes were to rollback the transaction then the TM instructs all the databases/servers to undo any changes made during the prepare phase. Finally, the TM can delete details about the just-completed transaction from its own persistent store.

If the system ever crashes then when the system is restarted, the TM examines its persistent storage of details about transactions in progress. Using this information, the TM can instruct databases/servers to rollback any transactions that had not reached the commit phase before the crash occurred. The TM can also replay the final outcome of the two-phase commit protocol for any transactions that were in the process of committing when the crash occurred.

The Open Group (www.opengroup.org), which is sometimes referred to as X/Open, has defined some open standards for distributed transactions. One of these standards, called XA, is a C-based API that a transaction manager can use to interact with a resource manager (most commonly a database) to implement the two-phase commit protocol. The importance of this standard is that it allows a transaction manager to coordinate a distributed transaction not just across multiple databases, but also across multiple brand names of databases. Section 21.4 will discuss how CORBA leverages the XA standard to allow CORBA applications to take part in distributed transactions. However, before discussing that, there are two other points worth noting.

First, when designing a client-server system, it is comforting to know that the middleware technology you are using makes it possible to use distributed transactions if the need arises. However, usually it is preferable to design the system in a way that uses only per-operation transactions rather than distributed transactions. One reason for this is that recovery after a crash during a local transaction is much less burdensome than recovery after a crash during a distributed transaction. Another reason is that local transactions tend to be short-lived so database locks are held for a minimum amount of time. Such short-lived transactions increase the possibility for concurrent access of the database and so promote system scalability. In contrast, distributed transactions tend to be longer-lived, especially if they involve user input, and this can limit concurrent access and scalability. A final reason is that distributed transactions have a higher overhead than local transactions (such as the overhead of logging and the extra communication required by the two-phase commit protocol).

Second, the Enterprise CORBA book [SGR99] devotes almost 100 pages to discussing the use of databases in client-server applications. If you plan on developing a client-server system that utilizes a database then you are advised to read that book for its wealth of useful design advice.

21.4  CORBA Object Transaction Service (OTS)

The Object Transaction Service (OTS) is a CORBA service (Section 1.6) that enables the use of distributed, two-phase commit transactions in CORBA applications. OTS consists of: (1) several IDL interfaces (most of which are defined in a module called CosTransactions), (2) some additional library code that is linked into client and server applications, and (3) a transaction manager. At first sight, the OTS specification can appear to be overly complex. There are two reasons for this.

The first reason for the apparent complexity of OTS is because the OTS specification defines not just the API that is used by “normal” developers; it also defines the lower-level “plumbing” API that is used by vendors to implement OTS. The reason why the OTS specification defines the plumbing API is that doing so ensures interoperability between different implementations of OTS. This means that an OTS client built with one CORBA product can take part in distributed transactions with OTS servers that are implemented with different CORBA products.

The second reason for the apparent complexity of OTS is because its API is flexible enough to allow transactional applications to be written in several different ways. Most developers use a simple API that allows them to focus on application-level logic, while leaving OTS to automatically perform several house-keeping tasks. However, OTS does allow developers to take a more hands-on approach and manually handle the required house-keeping tasks. The fuller set of “hands-on” APIs makes it possible for developers to integrate OTS with non-XA-compliant databases or to implement bridges from OTS to a non-CORBA distributed transactional system.


interface TransactionFactory { Control create(in unsigned long time_out); ... }; interface Control { Terminator get_terminator(); Coordinator get_coordinator(); }; interface Terminator { void commit(...); void rollback(); }; interface Coordinator { RecoveryCoordinator register_resource(in Resource r); ... }; interface RecoveryCoordinator { Status replay_completion(in Resource r); }; interface Resource { Vote prepare(); void rollback(); void commit(); ... }; local interface Current : CORBA::Current { void begin(); void commit(); void rollback(); void set_timeout(in unsigned long seconds); unsigned long get_timeout(); Control get_control(); Control suspend(); void resume(in Control which); };
Figure 21.3: A subset of the OTS APIs

Most of the OTS API is shown in Figure 21.3. A few details outside the scope of this chapter have been omitted from this figure. Also, the raises clause on operations have been omitted for brevity. Section 21.5 discusses this “raw” API of OTS. Then Section 21.6 discusses how OTS provides a simplified API by building on top of other functionality in CORBA.

21.5  The Raw API of OTS

The Resource interface is a CORBA “wrapper” around a resource (database). The operations defined on this interface are similar to the C-based API of the XA standard. Implementations of OTS provide an implementation of the Resource interface that trivially delegates to the underlying XA C-based API.1 This means that a server developer gets trivial integration between OTS and an XA-compliant database. If server developers are using a database that is not XA-compliant then they will have to implement the Resource interface for that database.

An implementation of OTS provides a transaction manager (TM). The specification does not state if this should be packaged as, say, a server process or as a library that can be linked into another application. However, it is common for the TM to be a stand-alone server process. Regardless of how it is packaged, the TM contains pre-written implementations of several interfaces: TransactionFactory, Control, Terminator, Coordinator and RecoveryCoordinator.

The CORBA specification does not state how an OTS client connects to the transaction factory in the TM, so the mechanism varies from one CORBA product to another. However, the connection is likely to be made by calling resolve_initial_references(). Use of this operation is discussed in Section 3.4.1. The OTS client calls TransactionFactory::create() to begin a transaction. This operation returns a reference to a Control object.

The client must somehow communicate the Control object reference when invoking an operation on an OTS-aware object. This could be achieved by explicitly passing the Control reference as a parameter to the operation. However, it is more commonly achieved by embedding the Control reference (along with other information) in a service context (Section 11.6) that is transmitted with the request. The OTS specification defines a service context structure for this purpose.

When the client wants to terminate a transaction, it calls Control::get_terminator() to obtain a reference to the Terminator object and then calls commit() or rollback() on this.

If an OTS server accesses an XA-compliant database then the server invokes an OTS operation (not shown in Figure 21.3) that puts a Resource wrapper around the database. If the server uses a non-XA-compliant database then the server developer must implement the Resource interface so that its database can take part in two-phase commit transactions.

In the original OTS specification, an object indicated that it could take part in OTS transactions by implementing an IDL interface that inherited from CosTransactions::TransactionalObject. The _is_a() operation (which is provided by the base Object type) was used by a client application to determine whether or not an object reference was for a transactionally aware object. However, the OMG decided that this approach was undesirable. In particular, it can result in a dramatic increase in the number of IDL interface definitions.2 Eventually, the OMG decided that it would be be better if whether or not an object was transactionally aware could be expressed as a quality of service. In modern versions of the OTS, this goal is achieved by defining a new POA Policy type (Section 6.1.4) that, if used, indicates that objects in that POA are transactionally aware. An IOR interceptor (Section 14.1) detects the presence of this POA policy and embeds an OTS TaggedComponent (Section 10.2.3) into IORs that originate from that POA. A client application can check for the presence of this TaggedComponent to determine if an object is transactionally aware.

When an operation in an OTS-enabled server receives a Control object, it can call get_coordinator() to gain access to the transaction’s Coordinator object. The Coordinator interface is a “wrapper” around the coordination logic that implements the two-phase commit protocol. Its purpose is to interact with the Resource objects in OTS servers. The server calls register_resource() on the Coordinator to register its resource (this registration occurs only once per transaction). This informs the TM that the server’s Resource is taking part in the transaction and so should be included in the two-phase commit protocol when the transaction commits. This operation returns a reference to a RecoveryCoordinator object for the transaction. The server stores this object reference in a persistent storage area so that if the server crashes during the two-phase commit protocol and is restarted then the server can contact the RecoveryCoordinator to determine if the transaction should commit or roll-back.

During the two-phase commit, the TM invokes the prepare() operation on all Resource objects that have taken part in the transaction. The return value of this operation is a Vote that determines if the transaction will be committed or rolled back.

21.6  How OTS Builds on Top of Other Parts of CORBA

This section briefly discusses a simple subset of the API provided by OTS. This simple subset is used by most OTS developers. The focus of this discussion is not to act as a tutorial for developers, but rather to show how other aspects of CORBA (such as current objects, portable interceptors and service contexts) are used as building blocks for more powerful capabilities, such as OTS.

OTS defines a Current object (Chapter 13). This object is accessed by calling resolve_initial_references("TransactionCurrent") (Section 3.4.1). The OTS Current object (defined in Figure 21.3) lets threads in both client and server applications know with which transaction they are currently associated.

An OTS client uses the begin(), commit() and rollback() operations on the Current object to control the lifetime of a transaction. Internally, the Current object delegates to the corresponding operations defined on the interfaces in the transaction manager. When a client invokes an operation on an object, a portable request interceptor (Section 14.2) provided by OTS embeds transactional context information obtained from the Current object in a service context (Section 11.6) that is then transmitted with the request to the target object. A corresponding portable request interceptor in the server extracts this transactional context information from the service context and initializes the server’s Current object before dispatching to the target operation. This means that the body of the operation executes within the context of a transaction. Because of this, the operation does not need to begin-and-commit or resume-and-suspend a transaction. Instead, these details are taken care of by the portable interceptor and so the body of the operation can focus on using, say, embedded SQL or JDBC to query/update the database.

The mechanism discussed above provides a simple API for developers and it is powerful enough for the majority of applications. However, developers can, if they so choose, avoid using the Current object and its associated portable interceptor, and instead manually execute their own OTS-infrastructure code. Although this is more complex, it provides a way for developers to integrate a non-XA-compliant database with OTS.


1
Readers who wish to do Java-based OTS development may wonder how Resource interacts with a Java DataBase Connectivity (JDBC) driver. The answer is that JDBC drivers provide XA-compliant DataSource objects. An implementation of the Resource interface delegates to an underlying DataSource object.
2
For example, the interfaces that define the Naming Service did not inherit from CosTransactions::TransactionalObject, which meant that an implementation of the Naming Service could not take part in a distributed transaction. To obtain an OTS-aware Naming Service would have required defining a new set of IDL interfaces that did inherit from CosTransactions::TransactionalObject.

Previous Up Next