WebLogic Server 6.0 Code Examples, BEA Systems, Inc.

Package examples.cluster.ejb

This example demonstrates how to write a clustered application with Enterprise JavaBeans that exhibits load balancing and failover.

See:
          Description

Class Summary
Client This class illustrates load balancing and failover using a container-managed JDBC EntityBean.
 

Package examples.cluster.ejb Description

This example demonstrates how to write a clustered application with Enterprise JavaBeans that exhibits load balancing and failover.

Note: You'll need a cluster license in order to run this example in a cluster environment. Contact your sales rep for more information.

In addition to the Client class, this example also uses:

  • Stateless session bean Teller (package examples.cluster.ejb.teller)
  • Entity bean Account (package examples.cluster.ejb.account)
  • Before running this example, you should run the other EJB examples as they will shows the steps that are involved for all the different types of EJBs. This is a complicated and sophisticated example that uses advanced techniques. It's assumed that you are familar with EJBs, have run the other examples and have written EJBs yourself.

    The example consists of a Client application that uses a stateless session EJB TellerBean. The TellerBean conducts transactions on behalf of the Client -- such as looking up an account balance, performing deposits, withdrawls and transfers -- using an entity EJB AccountBean.

    The example demonstrates:

    The Client application performs these steps:
    1. Contacts the Teller home ("examples.cluster.ejb.TellerHome") through JNDI to find the EJB's home
    2. Creates a Teller
    3. A loop is executed five times:
      1. Gets the balance of account "10000"
      2. Deposits an amount into account "10000"
      3. Withdraws a (smaller) amount from account "10000"
      4. Transfers an amount from account "10000" to account "10005"
      5. With each transaction, it reports balances and the server where the transaction occurred (where the Teller and Account beans are)
    4. Calculates the number of invocations on each server in the cluster, using the utility class examples.cluster.utils.ClusterUtils.
    The application demonstrates how the Client must decide in the event of a failure if a transaction should be retried or not. The Client determines this by asking a Teller to consult the transaction log (maintained in a separate table) and see if the transaction ID is there. If it is, the transaction was committed, irrespective of any exceptions. If not, the transaction can be attempted again if the exception received was a RemoteException, indicating a system failure.

    The statistics will show how all the invocations are distributed among the different servers that compose the cluster. Notice that the entity beans are colocated on the same server as the teller when called from within a transaction, and may appear on a different server when called outside of a transaction, such as looking up an account balance.

    Designing for Failover

    The example demonstrates how you design an application to failover and retry transactions when specific exceptions and conditions occur. This code snippet from the Client shows how the Client determines if the transaction was completed and if it should be retried:

        void transaction() throws Exception, TellerException, RemoteException, CreateException {
          trans++;
          int attempts      = 0;
          int set           = i;
          boolean committed = false;
          boolean invoke    = true;
          System.out.println("Transaction " + trans + " of set " + (set + 1) +
                             " (" + transId + ")");
          while (attempts < MAXATTEMPTS) {
            try {
              attempts++;
              System.out.println("  Attempt " + attempts);
              if (teller == null)
                teller = bank.create();
              if (invoke) {
                invokeTransaction();
                buildReport();
                printReport();
                System.out.println("  End of transaction " + trans + 
                                   " of set " + (set + 1));
                return;
              } else { // checking transaction
                committed = teller.checkTransactionId(transId);
                System.out.println("    Checked transaction " + transId + 
                                   ": committed = " + committed);
                if (committed) {
                  return;
                } else {
                  System.out.println("    Attempting Transaction " + trans + " again");
                  invoke = true;
                }
              }
            }
            catch (RemoteException re) {
              System.out.println("    Error: " + re);
              // Replace teller, in case that's the problem
              teller = null;
              invoke = false;
            }
          }
          throw new Exception("  Transaction " + trans + " of set " +
                              (set + 1) + " ended unsuccessfully");
        }

    Let's look at how the loop and exception handling occurs.

    A "while" loop is started and increments the number of attempts. It then creates a bean from the home (bank) if teller==null.

    If a Teller is sucessfully created, a transaction is tried. If that suceeds, the Teller will continue to be used for further transactions.

    If the transaction fails because of a RemoteException, the bank is asked for a new Teller (an idempotent method that can failover to another server), and the transaction is checked to see if it actually was committed or not.

    If the check returns that the transaction was not committed, an attempt is made to try the transaction again. Otherwise, the loop is exited. (You'll notice that we put the check inside the same loop as the call to invokeTransaction(), and in the client output, the call to the check will be described as an "Attempt". This is done to allow the check to failover to another server if necessary.)

    If a method on the Teller fails because of any other exception (such as a TellerException), it is assumed to be fatal and the loop is exited.

    The test attempts < MAXATTEMPT sets an upper bound on the loop. If it is reached, the Client will stop.

    In summary, when writing a clustered application with failover:

    Designing Logging

    An essential part of a clustered application that exhibts failover is being able to confirm that a transaction was successfully completed in the event of an exception. In this example, we do that by logging all the transactions in a database table, and including that logging as part of the transaction.

    The log table must reside on the same database server as the other table(s) that are used for EJB persistence, as the JTA driver used supports only one connection pool at a time. This connection pool is specified in the deployment descriptor for the TellerBean as "logPoolName oraclePool" and must be the same as the connection pool in the AccountBean.

    Methods in the TellerBean (setTransactionId() and checkTransactionId(d)) are used to write and check the log table. For efficiency, they directly access the database using database-specific SQL code.

    setTransactionId() writes to the log table in an LRU-fashion: it inserts records into the table until the table reaches a maximum size, and from then on updates the oldest existing record with the newest information. In practice, you'd need to size this table based on the number of transactions that you'd expect to log in the period of time between when a failure occured and when you'd want to check a transaction's status. Typically this would be a very short period of time, so the table would not have to be very large to handle a large number of transactions.

    There are a number of assumptions here:

    To get the most out of this example, first read through the source code files to see what is happening. Start with the deployment descriptors for the Teller and Account EJBs to find the general structure of the EJBs, which classes are used for the different objects and interfaces, then look at the Client code to see how the application works. You'll also need to configure an Oracle database for use as the persistent store.

    These three sections cover what to do:

    1. Build the example
    2. Configure the server
    3. Run the example

    Build the Example

    1. Open a new command shell to build the example EJBs.
    2. Set up your development shell as described in Setting up your environment.
    3. Move to the /samples/examples/cluster/ejb/account subdirectory:
      $ cd %WL_HOME%\samples\examples\cluster\ejb\account
    4. Compile the Account EJB and the example client using the supplied build script:
      $ build

      The build script builds the example Account EJB and places the resulting .jar files in the deployment directory of the examples server. See Building Enterprise JavaBean examples for more information on using the EJB build scripts. The Account build script also compiles the sample client application and places the compiled classes in the %WL_HOME%\config\examples\clientclasses directory.

      Note: BEA provides separate build scripts for Windows NT and UNIX: build.cmd and build.sh, respectively.

    5. Move to the /samples/examples/cluster/ejb/teller subdirectory:
      $ cd %WL_HOME%\samples\examples\cluster\ejb\teller
    6. Compile the Teller EJB using the supplied build script:
      $ build

    Configure the Server

    1. Configure a WebLogic Server cluster within the Examples domain, if you have not already done so.
    2. Start the clustered servers using the Examples configuration. You may want make a copy of the provided StartExamplesServer script for each server in your cluster, and edit each copy to boot an individual server.
    3. Bring up the Administration Console in your browser.
    4. Click to expand the JDBC node in the left-hand pane.
    5. Click to expand the Connection Pools node in the left-hand pane.
    6. Select the oraclePool node. The oraclePool configuration displays in the right-hand pane.
    7. Verify that the connection pool properties are valid for your Oracle database. In particular, make sure that the username, password, and server definitions are correct. If they are not correct, change them now and apply the changes.
    8. Select the Targets tab to display the Available and Chosen targets.
    9. Deploy oraclePool on your configured cluster.
    10. The database tables used by this example (ejbAccounts, used for the entity AccountBean and ejbTransLog, used for the transaction log table) must be created and exist in the database before the example is run. The SQL statements below (also found in the file oracle.ddl) create the tables and populate the ejbAccounts table:
        DROP TABLE ejbAccounts;
        CREATE TABLE ejbAccounts (id varchar(15), bal float, type varchar(15));
        INSERT INTO ejbAccounts (id,bal,type) VALUES ('10000',1000,'Checking');
        INSERT INTO ejbAccounts (id,bal,type) VALUES ('10005',1000,'Savings');
        DROP TABLE ejbTransLog;
        CREATE TABLE ejbTransLog (transId varchar(32), transCommitDate date);

      Create the tables in your database using the above SQL statements, or by using the oracle.ddl file with the Schema utility.

      Note: If you use a non-Oracle database, you may need to change both the SQL code given above and the SQL code in the TellerBean methods setTransactionId() and checkTransactionId().

    11. Click to expand the EJB Deployments node in the left-hand pane.
    12. Choose the cluster target for the cluster_ejb_account.jar and cluster_ejb_teller.jar files as follows:

    Run the Example

    1. Open a separate command-line window in which you will run the client.
    2. Set up the environment for your client as described in Setting up your environment for building and running the examples.
    3. Run the example client using the command:
      $ java examples.cluster.ejb.Client "t3://WebLogicClusterName:Port"

      where:

      WebLogicClusterName
      DNS cluster name for the WebLogic Cluster.
      Port
      Port that is listening for connections. (weblogic.system.ListenPort)

      The client application displays output similar to the following:

      Beginning examples.cluster.ejb.Client...
      
      Start of transaction set 1
      Transaction 1 of set 1 (921736154358)
        Attempt 1
          Teller (server1): balance of account
          Account 10000 (on server1): balance: $1000.0
        End of transaction 1 of set 1
      Transaction 2 of set 1 (921736160898)
        Attempt 1
          Teller (server3): deposited $100.0
          Account 10000 (on server3): balance: $1100.0
        End of transaction 2 of set 1
      Transaction 3 of set 1 (921736171934)
        Attempt 1
          Teller (server2): withdrew $50.0
          Account 10000 (on server2): balance: $1050.0
        End of transaction 3 of set 1
      Transaction 4 of set 1 (921736242936)
        Attempt 1
          Teller (server1): Transfered $50.0 from 10000 to 10005
          Account 10000 (on server1): balance: $1000.0
          Account 10005 (on server1): balance: $1050.0
        End of transaction 4 of set 1
      End of transaction set 1
      
      Start of transaction set 2
      Transaction 1 of set 2 (921736257437)
       .
       .
       .
      
      Statistics for different servers:
      
      "server3" processed 16 (36%) of 45 invocations
      "server2" processed 14 (31%) of 45 invocations
      "server1" processed 15 (33%) of 45 invocations
      
      End examples.cluster.ejb.Client...
      Notice how the invocation load is balanced across the different servers in the Cluster. Because the entity beans are colocated with the session beans, the load is not balanced identically on all servers, as one of the transactions calls two entity beans.

    4. To demonstrate failover from one server to the next, bring down one of the servers at an appropriate point while running the client application. You can bring down a server by either typing "Control-C" in the command-line window for the server (Windows) or by issuing a "kill" command using the process ID of the server to be brought down (UNIX).

      If you watch in the server output logs, you'll see lines such as these, followed by the server pausing:

      teller.transaction: tx.commit() about to be called: failover test point
      
      account.ejbStore (1724741, PK = 10000): failover test point

      These indicate that the server is about to invoke on an EJB, and are good locations to test failover.

      You should get output similar to this from the client application:

      D:\weblogic\src40_117\examples>Beginning examples.cluster.ejb.Client...
      
      Start of transaction set 1
      Transaction 1 of set 1 (921792857443)
        Attempt 1
          Teller (server1): balance of account
          Account 10000 (on server1): balance: $1400.0
        End of transaction 1 of set 1
      Transaction 2 of set 1 (921792862891)
        Attempt 1
          Error: java.rmi.RemoteException: ; nested exception is:
              weblogic.rjvm.PeerGoneException: JVM 7645352540945089822S172.17.12.34:[7
      001,7001,7002,7002] is gone
        Attempt 2
          Checked transaction 921792862891: committed = false
          Attempting Transaction 2 again
        Attempt 3
          Teller (server1): deposited $100.0
          Account 10000 (on server1): balance: $1500.0
        End of transaction 2 of set 1
      Transaction 3 of set 1 (921793006107)
        Attempt 1
          Teller (server3): withdrew $50.0
          Account 10000 (on server3): balance: $1450.0
        End of transaction 3 of set 1
      Transaction 4 of set 1 (921793016331)
        Attempt 1
          Teller (server1): Transfered $50.0 from 10000 to 10005
          Account 10000 (on server1): balance: $1400.0
          Account 10005 (on server1): balance: $2100.0
        End of transaction 4 of set 1
      End of transaction set 1
      
      Start of transaction set 2
       .
       .
       .
      Note how after one of the servers was killed as it was attempting to commit the transfer, the transaction was then checked, found to have not been committed, and was attempted again successfully. If the server was killed after the committ had successfully been sent to the database, the check of the transaction would have shown that the commit was successful, and the transaction would not be attempted again.

    There's More...

    Read more about:


    Documentation is available at
    http://e-docs.bea.com/wls/docs60

    Copyright © 2000 BEA Systems, Inc. All Rights Reserved.