3.3. Designing the Application Logic

Documentation

VoltDB Home » Documentation » Using VoltDB

3.3. Designing the Application Logic

Once you design your database schema, partitioning, and stored procedures, you are ready to write the application logic. Most of the logic and code of the calling programs are specific to the application you are designing. The important aspect, with regards to using VoltDB, is understanding how to:

  • Create a connection to the database

  • Call stored procedures

  • Close the client connection

The following sections explain how to perform these functions using the standard VoltDB Java client interface. The VoltDB Java client is a thread-safe class library that provides runtime access to VoltDB databases and functions.

It is possible to call VoltDB stored procedures from programming languages other than Java. However, reading this chapter is still recommended to understand the process for invoking and interpreting the results of a VoltDB stored procedure. See Chapter 15, Using VoltDB with Other Programming Languages for more information about using VoltDB from applications written in other languages.

3.3.1. Connecting to the VoltDB Database

The first step for the calling program is to create a connection to the VoltDB database. You do this by:

  1. Defining the configuration for your connections

  2. Creating an instance of the VoltDB Client class

  3. Calling the createConnection method

org.voltdb.client.Client client = null;
ClientConfig config = null;
try {
       config = new ClientConfig("advent","xyzzy");
       client = ClientFactory.createClient(config);
       client.createConnection("myserver.xyz.net");
} catch (java.io.IOException e) {
       e.printStackTrace();
       System.exit(-1);
}

In its simplest form, the ClientConfig class specifies the username and password to use. It is not absolutely necessary to create a client configuration object. For example, if security is not enabled (and therefore a username and password are not needed) a configuration object is not required. But it is a good practice to define the client configuration to ensure the same credentials are used for all connections against a single client. It is also possible to define additional characteristics of the client connections as part of the configuration, such as the timeout period for procedure invocations or a status listener. (See Section 3.4, “Handling Errors” for details.)

Once you instantiate your client object, the argument to createConnection specifies the database node to connect to. You can specify the server node as a hostname (as in the preceding example) or as an IP address. You can also add a second argument if you want to connect to a port other than the default. For example, the following createConnection call attempts to connect to the admin port, 21211:

client.createConnection("myserver.xyz.net",21211);

If security is enabled and the username and password in the ClientConfig do not match a user defined in the deployment file, the call to createConnection will throw an exception. See Chapter 8, Security for more information about the use of security with VoltDB databases.

When you are done with the connection, you should make sure your application calls the close method to clean up any memory allocated for the connection.

try {
    client.close();
} catch (InterruptedException e) {
    e.printStackTrace();
}

3.3.1.1. Connecting to Multiple Servers

You can create the connection to any of the nodes in the database cluster and your stored procedure will be routed appropriately. In fact, you can create connections to multiple nodes on the server and your subsequent requests will be distributed to the various connections. For example, the following Java code creates the client object and then connects to all three nodes of the cluster. In this case, security is not enabled so no client configuration is needed:

try {
       client = ClientFactory.createClient();
       client.createConnection("server1.xyz.net");
       client.createConnection("server2.xyz.net");
       client.createConnection("server3.xyz.net");
} catch (java.io.IOException e) {
       e.printStackTrace();
       System.exit(-1);
}

Creating multiple connections has three major benefits:

  • Multiple connections distributes the stored procedure requests around the cluster, avoiding a bottleneck where all requests are queued through a single host. This is particularly important when using asynchronous procedure calls or multiple clients.

  • For Java applications, the Java client library uses client affinity. That is, the client knows which server to send each request to based on the partitioning, thereby eliminating unnecessary network hops.

  • Finally, if a server fails for any reason, when using K-safety the client can continue to submit requests through connections to the remaining nodes. This avoids a single point of failure between client and database cluster.

3.3.1.2. Using an Auto-Reconnecting Client

If the client application loses contact with a server (either because the server goes down or a temporary network glitch), the connection to that server is closed. Assuming the application has connections to multiple servers in the cluster, it can continue to submit stored procedures through the remaining connections. However, the lost connection is not, by default, restored.

The application can use error handling to detect and recover from broken connections, as described in Section 3.4.3, “Interpreting Other Errors”. Or you can enable auto-reconnecting when you initialize the client object. You set auto-reconnecting in the client configuration before creating the client object, as in the following example:

org.voltdb.client.Client client = null;
ClientConfig config = new ClientConfig("","");
config.setReconnecOnConnectionLoss(true);
 try {
       client = ClientFactory.createClient(config);
       client.createConnection("server1.xyz.net");
       client.createConnection("server2.xyz.net");
       client.createConnection("server3.xyz.net");
            . . .

When setReconnectOnConnectionLoss is set to true, the client library will attempt to reestablish lost connections, attempts starting every second and backing off to every eight seconds. As soon as the connection is reestablished, the reconnected server will begin to receive its share of the procedure calls.

3.3.2. Invoking Stored Procedures

Once you create the connection, you are ready to call the stored procedures. You invoke a stored procedure using the callProcedure method, passing the procedure name and variables as arguments to callProcedure. For example, to invoke the LookupFlight stored procedure that requires three values (the originating airport, the destination, and the departure time), the call to callProcedure might look like this:

VoltTable[] results;
try { results = client.callProcedure("LookupFlight", 
                                     origin, 
                                     dest, 
                                     departtime).getResults();
} catch (Exception e) {
     e.printStackTrace();
     System.exit(-1);
}

Note that since callProcedure can throw an exception (such as VoltAbortException) it is a good practice to perform error handling and catch known exceptions.

Once a synchronous call completes, you can evaluate the results of the stored procedure. The callProcedure method returns a ClientResponse object, which includes information about the success or failure of the stored procedure. To retrieve the actual return values you use the getResults() method, as in the preceding example. See Section 3.2.3.4, “Interpreting the Results of SQL Queries” for more information about interpreting the results of VoltDB stored procedures.

3.3.3. Invoking Stored Procedures Asynchronously

Calling stored procedures synchronously can be useful because it simplifies the program logic; your client application waits for the procedure to complete before continuing. However, for high performance applications looking to maximize throughput, it is better to queue stored procedure invocations asynchronously.

To invoke stored procedures asynchronously, you use the callProcedure method with an additional argument, a callback that will be notified when the procedure completes (or an error occurs). For example, to invoke a procedure to add a new customer asynchronously, the call to callProcedure might look like the following:

client.callProcedure(new MyCallback(), `
                     "NewCustomer",
                     firstname, 
                     lastname, 
                     custID};

The callback procedure (MyCallback in this example) is invoked once the stored procedure completes. It is passed the same structure, ClientResponse, that is returned by a synchronous invocation. ClientResponse contains information about the results of execution. In particular, the methods getStatus and getResults let your callback procedure determine whether the stored procedure was successful and evaluate the results of the procedure.

The following is an example of a callback procedure:

static class MyCallback implements ProcedureCallback {
  @Override
  public void clientCallback(ClientResponse clientResponse) {

    if (clientResponse.getStatus() != ClientResponse.SUCCESS) {
       System.err.println(clientResponse.getStatusString());
   } else {
       myEvaluateResultsProc(clientResponse.getResults());
    }
  }
}

The VoltDB Java client is single threaded, so callback procedures are processed one at a time. Consequently, it is a good practice to keep processing in the callback to a minimum, returning control to the main thread as soon as possible. If more complex processing is required by the callback, creating a separate thread pool and spawning worker methods on a separate thread from within the async callback is recommended.

The following are other important points to note when making asynchronous invocations of stored procedures:

  • Asynchronous calls to callProcedure return control to the calling application as soon as the procedure call is queued.

  • If the database server queue is full, callProcedure will block until it is able to queue the procedure call. This is a condition known as backpressure. This situation does not normally happen unless the database cluster is not scaled sufficiently for the workload or there are abnormal spikes in the workload. Two ways to handle this situation programmatically are to:

    • Let the client pause momentarily to let the queue subside. The asynchronous clent interface does this automatically for you.

    • Create multiple connections to the cluster to better distribute asynchronous calls across the database nodes.

  • Once the procedure is queued, any subsequent errors (such as an exception in the stored procedure itself or loss of connection to the database) are returned as error conditions to the callback procedure.

3.3.4. Closing the Connection

When the client application is done interacting with the VoltDB database, it is a good practice to close the connection. This ensures that any pending transactions are completed in an orderly way. There are two steps to closing the connection:

  1. Call drain() to make sure all asynchronous calls have completed.

  2. Call close() to close all of the connections and release any resources associated with the client.

The drain() method pauses the current thread until all outstanding asynchronous calls (and their callback procedures) complete. This call is not necessary if the application only makes synchronous procedure calls. However, there is no penalty for calling drain() and so it can be included for completeness in all applications.

The following example demonstrates how to close the client connection:

try {
    client.drain();
    client.close();
} catch (InterruptedException e) {
    e.printStackTrace();
}
>