[Top] [Prev] [Next] [Bottom]

5. Global Transactions in BEA TUXEDO System


Introduction

The purpose of this chapter is to explain the concept of global transactions and how to define and manage them in your application using the ATMI primitives for transaction management.

A global transaction is a transaction that allows work involving more than one resource manager and spanning more than one physical site to be treated as one logical unit. The tpbegin() function allows you explicitly to start a transaction. The process that calls tpbegin() is the initiator of the transaction and must complete it by calling tpcommit() or tpabort(). Once a process is in transaction mode, any service requests made to servers may be processed on behalf of the current transaction. The services that are called and join the transaction are the participants. They may affect the outcome of the transaction by the value they return when they invoke the tpreturn() function. A process can determine if it is currently working on behalf of a transaction by calling the tpgetlev() function. The rest of this chapter will explain these functions in detail.

What Is a Global Transaction?

Before we get into how you can write applications that define and manage global transactions, this section gives you some idea as to what is meant by a transaction that is under the control of a transaction monitor.

The BEA TUXEDO system manages global transactions. As already indicated, a global transaction is one that can execute in more than one server, accessing data from more than one resource manager. A global transaction may be composed of several local transactions, each accessing a single resource manager. A local transaction accesses a single database or file and is controlled by the resource manager responsible for performing concurrency control and atomicity of updates at that distinct database. A given local transaction may be either successful or unsuccessful in completing its access.

A global transaction is always treated as a specific sequence of operations that is characterized by the four properties of atomicity, consistency, isolation, and durability. That is, it is a logical unit of work in which:

The BEA TUXEDO system is responsible for managing the status of the global transaction and making the decision as to whether or not a global transaction should be committed or rolled back. Global transactions are explicitly defined and controlled by the ATMI function primitives that can be found on their respective reference pages in the BEA TUXEDO Reference Manual and are the topic of this chapter. More specifically, the ATMI functions enable the application programmer to begin and terminate transactions and to test if a client or service routine is currently in a transaction.

ATMI Transaction Primitives

The ATMI primitives tpbegin(), tpcommit(), and tpabort() are used to explicitly begin and end a transaction. The initiator of a transaction uses tpbegin() to mark its beginning. After specifying the operations (service requests) to be applied to the resource as part of this transaction, the initiator can then call either tpcommit() or tpabort() to mark its completion. The calls to initiate and terminate a transaction delineate the operations within the transaction. If the transaction is completed with a call to tpcommit(), the changes made as a result of the transaction are applied to the resource and become permanent. tpabort() causes the resource to be in the consistent state at the start of the transaction. That is, any changes made to the resource are rolled back. Any of the participants of a transaction can cause the global transaction to fail by communicating their local failure to the initiator through the tpreturn() function. A two-phase commit protocol is used by BEA TUXEDO to coordinate the commitment, rollback, and recovery of global transactions. This protocol will be further discussed later in the chapter.

When the tpgetlev() function is invoked, it returns a 1 or a 0 that indicates if the caller is within a transaction (1) or not (0).

Explicitly Defining a Global Transaction

Global transactions can be defined in either client or server processes. To explicitly define a global transaction, call the tpbegin() function. Follow it by the program statements that are to be in transaction mode. Terminate the statements by a call to tpcommit() or tpabort().

The three functions have the following syntax:

int
tpbegin(timeout, flags) /* Begin transaction */
unsigned long timeout;
long flags;

int
tpcommit(flags) /* Commit current transaction */
long flags;

int
tpabort(flags) /* Abort current transaction */
long flags;

A high-level view of defining a transaction is shown in Listing 5-1.

Listing 5-1 Delineating a Transaction
. . .
if (tpbegin(timeout,flags) == -1)
error routine
program statements
. . .
if (tpcommit(flags) == -1)
error routine

The process that makes the call to tpbegin(), the initiator, must also be the one that terminates it by invoking either tpcommit() or tpabort(). There is no limit to the number of sequential transactions that a process may define using these functions. Any process may call tpbegin() except if it is already in transaction mode. If tpbegin() is called in transaction mode, the call will fail because of an error in protocol and tperrno will be set to TPEPROTO. If the process is in transaction mode, the transaction is unaffected by the failure.

Any service subroutines that are called within the transaction delimiters of tpbegin() and tpcommit()/tpabort() become part of the current transaction. However, if tpcall() or tpacall() have the flags parameter explicitly set to TPNOTRAN, the operations performed by the called service do not become part of that transaction. This in effect means that the calling process is not inviting the called service to be a participant in the current transaction. As a result, any services performed by the called process will not be affected by the outcome of the current transaction. It should be noted here that a call made with TPNOTRAN set that is directed to a service in an XA-compliant server group may produce unexpected results. See the discussion under "Implicitly Defining a Global Transaction" later in this chapter.

Starting the Transaction

The transaction is started by a call to tpbegin(). tpbegin() takes two parameters, only one of which is used at the present time. timeout specifies the amount of time in seconds a transaction has before timing out; flags is currently undefined and must be set to 0. The value of the timeout parameter indicates the least amount of time in seconds that a transaction should be given before timing out. If 0 is specified for this parameter, the transaction is given the maximum number of seconds allowed by the system before timing out (that is, the time-out value will equal the maximum value for an unsigned long as defined by the system).

Note: The use of 0 or unrealistically large values for the timeout parameter delays system detection and reporting of errors. A time-out value is used to ensure response to service requests within a reasonable time, and to terminate transactions that have encountered problems such as network failures prior to commit. For a transaction in which a person is waiting for a response, a small value, often less than 30 seconds, is best. In a production system, the time-out value should be large enough to accommodate expected delays due to system load, and database contention; a small multiple of the expected average response time is often an appropriate choice.

If a transaction times out, it is aborted. You can determine if a transaction has timed out by testing the value of tperrno as illustrated in Listing 5-2. Note that if the transaction timed out and it goes untested, a call to tpcommit() will still cause the transaction to be aborted. In this case, tpcommit() fails and returns TPEABORT tpcommit() in tperrno and the transaction is implicitly aborted.

The value assigned to the timeout parameter should be consistent with the SCANUNIT parameter set by the BEA TUXEDO system administrator in the configuration file. The system parameter specifies the frequency with which timed-out transactions and blocked calls are looked for. Its value represents an interval of time between periodic scans to find old transactions and timed out blocking calls within service requests. The timeout parameter should be set to a value that is greater than the scanning unit. If the time-out value were smaller, there would be some discrepancy between the time the transaction timed out and its discovery. The default value for SCANUNIT is 10 seconds. The value you give to timeout may need to be coordinated with your system administrator to be sure it makes sense with regard to the system parameters.

Listing 5-2 illustrates the starting of a transaction with the time-out value set to 30 seconds followed by a check to see if a timeout occurred.

Listing 5-2 Testing for Transaction Timeout
if (tpbegin(30, 0) == -1) {
(void)userlog("%s: failed to begin transaction\n", argv[0]);
tpterm();
exit(1);
}
. . .
communication calls
. . .
if (tperrno == TPETIME){
if (tpabort(0) == -1)
check for errors;
}
else if (tpcommit(0) == -1){
check for errors;
}
. . .

Note that a transaction is still subject to timing out even when a process calls on another with the TPNOTRAN communication flag set. This will be further discussed in Chapter 7, "Error Management."

The example in Listing 5-3 is excerpted from the audit.c client program of the banking application.

Listing 5-3 Defining a Transaction
#include <stdio.h>         /* UNIX */
#include <string.h> /* UNIX */
#include <atmi.h> /* TUXEDO */
#include <Uunix.h> /* TUXEDO */
#include <userlog.h> /* TUXEDO */
#include "bank.h" /* BANKING #defines */
#include "aud.h" /* BANKING view defines */

#define INVI 0 /* account inquiry */
#define ACCT 1 /* account inquiry */
#define TELL 2 /* teller inquiry */

static int sum_bal _((char *, char *));
static long sitelist[NSITE] = SITEREP; /* list of machines to audit */
static char pgmname[STATLEN]; /* program name = argv[0] */
static char result_str[STATLEN]; /* string to hold results of query */

main(argc, argv)
int argc;
char *argv[];
{
int aud_type=INVI; /* audit type -- invalid unless specified */
int clarg; /* command line arg index from optind */
int c; /* Option character */
int cflgs=0; /* Commit flags, currently unused */
int aflgs=0; /* Abort flags, currently unused */
int nbl=0; /* count of branch list entries */
char svc_name[NAMELEN]; /* service name */
char hdr_type[NAMELEN]; /* heading to appear on output */
int retc; /* return value of sum_bal() */
struct aud *audv; /* pointer to audit buf struct */
int audrl=0; /* audit return length */
long q_branchid; /* branch_id to query */

. . . /* Get Command Line Options and Set Variables */

/* Join application */

if (tpinit((TPINIT *) NULL) == -1) {
(void)userlog("%s: failed to join application\n", pgmname);
exit(1);
}

/* Start global transaction */

if (tpbegin(30, 0) == -1) {
(void)userlog("%s: failed to begin transaction\n", pgmname);
(void)tpterm();
exit(1);
}

if (nbl == 0) { /* no branch id specified so do a global sum */
retc = sum_bal(svc_name, hdr_type); /* sum_bal routine not shown */

} else {

/* Create buffer and set data pointer */

if ((audv = (struct aud *)tpalloc("VIEW", "aud", sizeof(struct aud)))
== (struct aud *)NULL) {
(void)userlog("audit: unable to allocate space for VIEW\n");
exit(1);
}

/* Prepare aud structure */

audv->b_id = q_branchid;
audv->balance = 0.0;
audv->ermsg[0] = '\0';

/* Do tpcall */

if (tpcall(svc_name,(char *)audv,sizeof(struct aud),
(char **)audv,(long *)audrl,0) == -1){
(void)fprintf (stderr,"%s service failed\n%s: %s\n",
svc_name, svc_name, audv->ermsg);
retc = -1;

}else {

(void)sprintf(result_str,"Branch %ld %s balance is $%.2f\n",
audv->b_id, hdr_type, audv->balance);
}
tpfree((char *)audv);
}

/* Commit global transaction */

if (retc < 0) /* sum_bal failed so abort */
(void) tpabort(aflgs);
else {
if (tpcommit(cflgs) == -1) {
(void)userlog("%s: failed to commit transaction\n",
pgmname);
(void)tpterm();
exit(1);
}
/*print out results only when transaction has committed successfully*/
(void)printf("%s",result_str);
}

/* Leave application */

if (tpterm() == -1) {
(void)userlog("%s: failed to leave application\n", pgmname);
exit(1);
}

Terminating the Transaction

As already indicated, a transaction is terminated by a call to either tpcommit() or tpabort(). Both have the flags parameter defined. However, while flags is not currently used, you must set this parameter to zero to ensure compatibility with future releases. When tpcommit() returns successfully, all changes to the resource as a result of the current transaction become permanent. tpabort() is called to indicate an abnormal condition and explicitly aborts the transaction and invalidates the call descriptors of any outstanding transactional replies (calls done with the TPNOTRAN flag will not be invalidated). None of the changes that were produced as a result of the transaction are applied to the resource. For tpcommit() to succeed, the following two conditions must be true:

If either condition is not true, the call fails and tperrno is set to TPEPROTO indicating an error in protocol. If a participant calls tpcommit() or tpabort(), the transaction is unaffected. If tpcommit() is called by the initiator with outstanding transaction replies, the transaction is aborted and those reply descriptors associated with the transaction become invalid.

tpcommit Initiates the Two-Phase Commit

When tpcommit() is called, it initiates the two-phase commit protocol mentioned earlier. This protocol, as the name suggests, has two parts. In the first, each participating resource manager indicates a readiness to commit. In the second, the initiator gives permission to commit. The process that calls tpcommit() must be the initiator of the transaction. As the initiator, this process starts the commit processing in which the participants (the other server processes that took part in the transaction) communicate their success or failure. This can be made known to the initiator by tpreturn() through the rval parameter that can be set to either TPSUCCESS or TPFAIL. If TPFAIL has been returned, tpcommit() fails, tperrno is set to TPEABORT, and the transaction is implicitly aborted. All the work that is performed by every process that participated in that transaction is undone. More will be said about the transaction role of tpreturn() and TPFORWAR() in Chapter 7, "Error Management."

Setting When tpcommit() Should Return

When more than one machine is involved in a transaction, the application can elect to specify that tpcommit() should return successfully when all participants have indicated a readiness to commit; that is, when phase 1 of the two-phase commit has been logged as complete by all participants. The alternative choice is to have tpcommit() wait until all participants have finished phase 2 of the two-phase commit. The CMTRET parameter in the RESOURCES section of UBBCONFIG can be set to either LOGGED or COMPLETE to control this characteristic. The function TPSCMT() can be called with its flags argument set to either TP_CMT_LOGGED or TP_CMT_COMPLETE to override the setting in the configuration file.

The idea behind this option is that most of the time when all participants in a global transaction have logged successful completion of phase 1, they will not fail to complete phase 2. By setting TP_COMMIT_CONTROL to LOGGED you allow slightly faster return of calls to tpcommit(), but you run the slight risk that a participant (probably on a remote node) may heuristically complete its part of the transaction in a way that is not consistent with the commit decision. Whether it is prudent to accept the risk depends to a large extent on the nature of your application. If your application demands complete accuracy (for example, if you are running a financial application) you would probably prefer to allow for the time required for all participants fully to complete the two-phase commit process. If you are counting beans, you may prefer to have the application run as fast as possible even knowing you may be a few beans off over a period of time.

Testing for Participant Errors

A client making a synchronous call to the fictitious REPORT service (line 18) is shown in Listing 5-4. It demonstrates testing for errors that can be returned on a communication call that indicate participant failure (lines 19-34).

Listing 5-4 Testing for Participant Success or Failure
001    #include <stdio.h>
002 #include "atmi.h"
003
004 main()
005 {
006 char *sbuf, *rbuf;
007 long slen, rlen;
008 if (tpinit((TPINIT *) NULL) == -1)
009 error message, exit program;
010 if (tpbegin(30, 0) == -1)
011 error message, tpterm, exit program;
012 if ((sbuf=tpalloc("STRING", NULL, 100)) == NULL)
013 error message, tpabort, tpterm, exit program;
014 if ((rbuf=tpalloc("STRING", NULL, 2000)) == NULL)
015 error message, tpfree sbuf, tpabort, tpterm, exit program;
016 (void)strcpy(sbuf, "REPORT=accrcv DBNAME=accounts");
017 slen=strlen(sbuf);
018 if (tpcall("REPORT", sbuf, slen, &rbuf, &rlen, 0) == -1) {
019 switch(tperrno) {
020 case TPESVCERR:
021 fprintf(stderr,
022 "REPORT service's tpreturn encountered problems\n");
023 break;
024 case TPESVCFAIL:
025 fprintf(stderr,
026 "REPORT service TPFAILED with return code of %d\n", tpurcode);
027 break;
028 case TPEOTYPE:
029 fprintf(stderr,
030 "REPORT service's reply is not of any known data type\n");
031 break;
032 }
033 if (tpabort(0) == -1){
034 check for errors;
035 }
036 }
037 else
038 if (tpcommit(0) == -1)
039 fprintf(stderr, "REPORT failed at commit time\n");
040 tpfree(rbuf);
041 tpfree(sbuf);
042 tpterm();
043 exit(0);
044 }

Committing a Transaction in Conversational Mode

Figure 5-1 shows a conversational connection hierarchy that includes a global transaction. The originator of a connection in transaction mode (process A that called tpbegin() followed by tpconnect()) can call tpcommit() after all services have called tpreturn(). If a hierarchy of connections exists as it does in Figure 5-1, each subordinate service must call tpreturn() when it no longer has replies outstanding. A TPEV_SVCSUCC or TPEV_SVCFAIL event is sent back up the hierarchy to the process that began the transaction. If all subordinates return successfully, the client (process A) completes the transaction; otherwise the transaction is aborted.

Figure 5-1 Connection Hierarchy: Transaction Mode

Implicitly Defining a Global Transaction

Besides using the ATMI primitives explicitly to start and end a transaction, it is possible for a global transaction to be started in two other ways.

In a Client Process

The BEA TUXEDO system provides a predefined client program that is part of its Data Entry System. The program is called mio and is the form handler for the system. Through the form specification language, UFORM, it is possible to cause a transaction to be started in mio whenever a service request is generated. The TRANMODE parameter in the FORM statement of the UFORM specification language allows you to have the client process initiate a transaction for each service. In this case, the programmer does not make explicit calls to tpbegin() or tpcommit()/tpabort() to delineate the transaction. If the TRANMODE parameter of the FORM statement has been set to TRAN, mio automatically starts and ends the transaction. The application logic to decide whether to commit or roll back the transaction is built into mio.

In a Service Routine

In another special case, a service routine can be placed in transaction mode through the system parameter, AUTOTRAN, in the configuration file. If AUTOTRAN is set to Y, a transaction is automatically started in the service subroutine when a request message is received from another process. Let's look at some variations on this theme.

Because a service can automatically be placed in transaction mode, it is possible for the call to be made with the communication flag set to TPNOTRAN and the flags member of the service information structure to return TPTRAN when queried. For example, if the call is made with the communication flags set to TPNOTRAN|TPNOREPLY and the service automatically starts a transaction when called, the flags member of the information structure will be set to TPTRAN|TPNOREPLY.

What a Service in an XA-Compliant Server Group Expects

A service that is part of an XA-compliant server group is generally written to perform some operation via the group's resource manager, which automatically opened the associated database when the application was booted. In the normal case, the service expects to do its work within a transaction. If a service like this is called with the caller's communication flags set to TPNOTRAN, the results of the ensuing database operation may be a little strange.

The solution is to write your application so that services in groups associated with XA-compliant resource managers are always called in transaction mode or are always defined in the configuration file with AUTOTRAN=Y. Another precaution is to test early in the service code to see what the transaction level is.

Testing Whether a Transaction Has Begun

In order correctly to interpret the error messages that can occur, it is important to know if a process is in transaction mode or not. It is an error for a process that is already in transaction mode to make a call to tpbegin(). tpbegin() will fail and set tperrno to TPEPROTO to indicate that the function was invoked while the caller was already in a transaction. However, the transaction will not be affected.

It might be helpful to think of transaction mode as something that is propagated unless specifically suppressed. When one process in transaction mode calls on the services of another process, that process acquires the same "condition." If mio has been placed in transaction mode through the form specification language, all the service subroutines it calls upon may be placed in transaction mode.

Service subroutines can be written so that they test to see if they are already in transaction mode before invoking tpbegin(). Testing transaction level can be done by querying the flags member of the service information structure that is passed to the service routine. If its value is set to TPTRAN, the service is in transaction mode. Also, this information can be retrieved by calling the tpgetlev() function. The syntax of this function is:

int
tpgetlev() /* Get current transaction level */

tpgetlev() returns 0 if the caller is not in a transaction and 1 if it is.

Listing 5-5 is a variation of the OPEN_ACCT service that shows testing for transaction level using the tpgetlev() function (line 12). If the process is not in transaction mode, it starts one (line 14). If tpbegin() fails, a message is returned to the status line (line 16) and the rcode argument of tpreturn() is set to a code that can be retrieved in the global variable tpurcode (line 17 and line 1).

If the AUTOTRAN configuration parameter discussed above is set to Y, you avoid the overhead of testing for transaction level and the need of explicitly calling the tpbegin() and tpcommit()/tpabort() transaction functions. For example, in the fragment shown in Listing 5-5, if OPEN_ACCT service is always to be called in transaction mode, the system parameters AUTOTRAN and TRANTIME can be set in the configuration file, eliminating the need to define the transaction or determine its existence within the programming code (lines 7 and 10-19).

Listing 5-5 Testing Transaction Level
001 #define BEGFAIL    3    /* tpurcode setting for return if tpbegin fails */

002 void
003 OPEN_ACCT(transb)

004 TPSVCINFO *transb;

005 {
... other declarations ...
006 FBFR *transf; /* fielded buffer of decoded message */
007 int dotran; /* checks whether service tpbegin/tpcommit/tpaborts */

008 /* set pointer to TPSVCINFO data buffer */

009 transf = (FBFR *)transb->data;

010 /* Test if transaction exists; initiate if no, check if yes */

011 dotran = 0;
012 if (tpgetlev() == 0) {
013 dotran = 1;
014 if (tpbegin(30, 0) == -1) {
015 Fchg(transf, STATLIN, 0,
016 "Attempt to tpbegin within service routine failed\n");
017 tpreturn(TPFAIL, BEGFAIL, transb->data, 0, 0);
018 }
019 }
. . .



[Top] [Prev] [Next] [Bottom]