6.6. Handling Errors

Documentation

VoltDB Home » Documentation » Using VoltDB

6.6. Handling Errors

A special situation to consider when calling VoltDB stored procedures is error handling. The VoltDB client interface catches most exceptions, including connection errors, errors thrown by the stored procedures themselves, and even exceptions that occur in asynchronous callbacks. These error conditions are not returned to the client application as exceptions. However, the application can still receive notification and interpret these conditions using the client interface.

The following sections explain how to identify and interpret errors that occur when executing stored procedures and in asynchronous callbacks. These include:

6.6.1. Interpreting Execution Errors

If an error occurs in a stored procedure (such as an SQL constraint violation), VoltDB catches the error and returns information about it to the calling application as part of the ClientResponse class. The ClientResponse class provides several methods to help the calling application determine whether the stored procedure completed successfully and, if not, what caused the failure. The two most important methods are getStatus() and getStatusString().

If you are using synchronous procedure calls in your application, a response whose status shows a failure will be turned into a ProcCallException. Call the exception's getClientResponse() method to retrieve the ClientResponse.

If you are using asynchronous procedure calls in your application, the response is always obtained from the completed CompletableFuture.

For both synchronous and asynchronous calls, errors that occur locally in the Client2 library may result in an exception being thrown, for example an IOException. You should establish appropriate handlers so that your application is robust.

callProcedureAsync("MyProc", "someArg")
        .thenAccept((resp) -> processResponse(resp))       1
        .exceptionally((th) -> processException(th));
          ...

void processResponse(ClientResponse resp) {
    final byte AppCodeWarm = 1;
    final byte AppCodeFuzzy = 2;
    if (resp.getStatus() != ClientResponse.SUCCESS) {      2
        System.err.println(resp.getStatusString());        3
    } else {
        if (resp.getAppStatus() == AppCodeFuzzy) {         4
           System.err.println(resp.getAppStatusString());
        }
        myEvaluateResultsProc(resp.getResults());
    }
}

void processException(Throwable th) {
    System.err.println("Exception: " + th);
}

1

thenAccept and exceptionally are just two examples of the many ways of establishing handlers that will be executed when the asynchronous procedure call completes. See the Java documentation for CompletableFuture.

2

The getStatus() method tells you whether the stored procedure completed successfully and, if not, what type of error occurred. It is good practice to always check the status of the ClientResponse before evaluating the results of a procedure call, because if the status is anything but SUCCESS, there will not be any results returned. Some possible values of getStatus() are:

  • CLIENT_REQUEST_TIMEOUT — The request timed out in the client before it could be sent to the cluster.

  • CLIENT_RESPONSE_TIMEOUT — The request timed out in the client after it had been sent to the cluster. The stored procedure may or may not have been executed. It may or may not have completed execution. See Section 6.6.2, “Handling Timeouts” for more information about handling this condition.

  • CONNECTION_LOST — The network connection was lost before the stored procedure returned status information to the calling application. The stored procedure may or may not have been executed. It may or may not have completed execution.

  • GRACEFUL_FAILURE — An error occurred and the stored procedure was gracefully rolled back.

  • RESPONSE_UNKNOWN — This is a rare error that occurs if the coordinating node for the transaction fails before returning a response. The node to which your application is connected cannot determine if the transaction failed or succeeded before the coordinator was lost. The best course of action, if you receive this error, is to use a new query to determine if the transaction failed or succeeded and then take action based on that knowledge.

  • SUCCESS — The stored procedure completed successfully.

  • UNEXPECTED_FAILURE — An unexpected error occurred on the server and the procedure failed.

  • USER_ABORT — The code of the stored procedure intentionally threw a UserAbort exception and the stored procedure was rolled back.

3

If a getStatus() call identifies an error status other than SUCCESS, you can use the getStatusString() method to return a text message providing more information about the specific error that occurred.

4

If you want the stored procedure to provide additional information to the calling application, there are two more methods to the ClientResponse that you can use. The methods getAppStatus() and getAppStatusString() act like getStatus() and getStatusString(), but rather than returning information set by VoltDB, getAppStatus() and getAppStatusString() return information set in the stored procedure code itself.

In the stored procedure, you can use the methods setAppStatusCode() and setAppStatusString() to set the values returned to the calling application by the stored procedure. For example:

/* stored procedure code */
final byte AppCodeWarm = 1;
final byte AppCodeFuzzy = 2;
              . . .
setAppStatusCode(AppCodeFuzzy);
setAppStatusString("I'm not sure about that...");
              . . .

6.6.2. Handling Timeouts

One particular error that needs special handling is if a connection or a stored procedure call times out after is has been sent to the cluster. By default, the client interface only waits a specified amount of time (two minutes) for a stored procedure to complete. If no response is received from the server before the timeout period expires, the client interface returns control to your application, notifying it of the error. The status in the ClientResponse object will be CLIENT_RESPONSE_TIMEOUT.

It is important to note that CLIENT_RESPONSE_TIMEOUT does not necessarily mean the procedure call failed. In fact, it is very possible that the procedure may complete and return information after the timeout error is reported. The timeout is provided to avoid locking up the client application when procedures are delayed or the connection to the cluster hangs for any reason.

Similarly, if no response of any kind is returned on a connection (even if no transactions are pending) within the specified timeout period, the client connection will time out. When this happens, the connection is closed, and any open stored procedures on that connection are closed with a return status of CONNECTION_LOST. Unlike a procedure timeout, when the connection times out, the connection no longer exists, so your client application will receive no further notifications concerning pending procedures, whether they succeed or fail.

CONNECTION_LOST does not necessarily mean a pending procedure call failed. It is possible that the procedure completed but was unable to return its status due to a connection failure. The goal of the connection timeout is to notify the client application of a lost connection in a timely manner, even if there are no outstanding procedures using the connection.

There are several things you can do to address potential timeouts in your application:

  • Change the timeout period by calling either or both the methods procedureCallTimeout() and connectionResponseTimeout() on the Client2Config object. The default timeout period is 2 minutes for both procedures and connections. A value of zero or less disables the timeout altogether. For example, the following client code resets the procedure timeout to 90 seconds and the connection timeout period to 3 minutes, or 180 seconds:

    config = new Client2Config()
            .procedureCallTimeout(90, TimeUnit.SECONDS)
            .connectionResponseTimeout(3, TimeUnit.MINUTES);
    client = ClientFactory.createClient(config);
  • Respond to the timeout error as part of the response to a procedure call. For example, the following code reports the error to the console.

    void processResponse(ClientResponse response) {
        if (response.getStatus() == ClientResponse.RESPONSE_CONNECTION_TIMEOUT) {
            System.out.println("A procedure invocation has timed out.");
            return;
        }
        if (response.getStatus() == ClientResponse.CONNECTION_LOST) {
            System.out.println("Connection lost before procedure response.");
            return;
        }
        // ... further non-timeout processing here ...
    }
  • Set up a handler to receive the results of any procedure invocations that complete after the client interface times out. See the following Section 6.6.3, “Writing Handlers to Interpret Other Errors” for an example of creating a handler for delayed procedure responses.

6.6.3. Writing Handlers to Interpret Other Errors

Certain types of errors can occur that the ClientResponse class cannot notify you about immediately. In these cases, an error happens and is caught by the client interface outside of the normal stored procedure execution cycle. If you want your application to address these situations, you need to create handlers, which are each a special type of asynchronous callback that the client interface will notify whenever such errors occur.

Handlers are individually defined for each type of event, by a specific call to a Client2Config method. The application handler must implement the corresponding interface, as defined in the Client2Notification class. The types of event that handlers address include:

Connect Failure

Notification that an attempt to connect or reconnect to a cluster member has failed. This applies to both initial connection requests from the application, and to automatic reconnection attempts by the Client2 implementation.

Connection Up

Notification that a connection has been established to a particular cluster member.

Connection Down

Notification that the connection to a particular cluster member has become disconnected. Outstanding procedure calls may or may not have completed successfully.

Late Response

Procedure invocations that time out in the client may later complete on the server and return results. Since the client application can no longer react to this response inline, the client may want a way to process the returned results. A ClientResponse object will be provided as an argument when this notification is called.

Request Backpressure

Indicates that the procedure-call queue in the Client2 API is nearing its limit. The queue has two limits: a warning limit and a hard limit. This notification is triggered when the queue length exceeds the warning limit. If the hard limit is exceeded, further procedure calls are rejected until the queue length falls sufficiently. The limits can be set in the Client2Config> with clientRequestBackpressureLevel() and ClientRequestLimit, for warning and hard limits respectively.

Error Log

Certain unlikely error situations in the Client2 API are not directly associated with a procedure call. By default, a message is printed on standard error. The application can choose to handle the message instead, perhaps writing it to its own log. The application should not attempt to interpret the message; the wording may change without notice.

For the sake of example, the following late-response handler does little more than display a message on standard output. However, in real world applications the handler would take appropriate actions based on the circumstances.

/*
 * Define a late response handler.
 */
void myLateResponseHandler(ClientResponse resp, String host, int port) {  1
    System.out.printf("A procedure that timed out on host %s port %d"
        + " has now responded with status %d\n", host, port, resp.getStatus());
}

/*
 *  Declare the client configuration, specifying
 *  a username, a password, and the handler.
 */
Client2Config myconfig = new Client2Config()                              2
        .username("username")
        .password("password")
        .lateResponseHandler(this::myLateResponseHandler);

/*
 *  Create the client using the specified configuration.
 */
Client2 myclient = ClientFactory.createClient(myconfig);                  3

By performing the operations in the order as described here, you ensure that all connections to the VoltDB database cluster use the same credentials for authentication and will notify the status listener of any error conditions outside of normal procedure execution.

1

Declare a late-response handler. Define the handler before you define the VoltDB client or open a connection. The handler signature must conform to the interface defined for this handler in Client2Notification, with the three formal arguments shown.

2

Define the client configuration Client2Config object. After you declare your ClientStatusListenerExt, you create a Client2Config object to use for all connections, which includes the username, password, and late-response handler. This configuration is then used to define the client.

3

Create a client with the specified configuration.