Once the application connects to the database you are ready to make procedure calls. At its simplest, you can make a synchronous call that returns the traditional response object:
ClientResponse resp = client.callProcedureSync("AddUser", username, password);
However, asynchronous calls are where the new Client2 excels. Asynchronous calls return a CompletableFuture (a standard Java object) which can then be used to process the response. For example, you can assign different actions based on the results of the call:
CompletableFuture<ClientResponse> addUserCB; addUserCB = client.callProcedure("AddUser",username,password); addUserCB.whenComplete( (resp,th) -> processResponse( resp ) );
Alternately, it is possible to hand off the response to a processing thread:
client.callProcedureAsync("AddUser", username, password) .thenAcceptAsync((resp) -> processResponse(resp)) .exceptionally((th) -> processException(th));
The Client2 interface improves the handling of backpressure; that is, when requests are submitted faster than they can be processed. It is possible for an application to submit requests faster than the client API or the VoltDB server can handle them, particularly when using asynchronous calls. To handle this situation, the API tracks all pending requests: this includes requests queued in the API, requests in the network layer, and requests sent to the VoltDB cluster but which have not been completed yet. The application can configure three limits on the number of requests pending in the client:
A high and low watermark, set using .clientRequestBackpressureWarning. If the number of outstanding requests exceeds the high watermark, the application is notified to slow down. If the number of outstanding requests then falls below the low watermark, the application is notified that it can resume normal operation.
A maximum number of requests, set using .clientRequestLimit. Once the client reaches this limit, subsequent procedure invocations are rejected until the request count drops below the limit again.
Additionally, there is a configurable limit on the number of transactions that can be outstanding at the VoltDB cluster, set using .outstandingTransactionLimit. For example:
config.clientRequestBackpressureWarning(2500, 1000) .clientRequestLimit(3000) .outstandingTransactionLimit(1000);
This example defines the following rules:
When the number of pending requests goes up to 2500, the application is told to slow down. This is the high watermark.
When the number or requests falls to 1000 or below, the application is told it can resume normal operation. This is the low watermark.
If the number of pending requests exceeds 3000, callProcedure will refuse to accept any more requests.
If the number of outstanding requests submitted to the cluster without a response exceeds 1000, the client automatically adds additional requests to the client queue.
Notifications for the high and low watermarks are delivered to the application via a callback defined as part of the client configuration with the .requestBackpressureHandler() method:
config.requestBackpressureHandler(myQueueWarning);
If you want to provide additional context to the event handler, you can define the callback as a local function within a class and use the "this::" syntax, so the callback has access to any context available to the class:
config.requestBackpressureHandler(this::myBackpressureHandler);
The callback itself identifies the particular condition by way of a boolean argument it receives. For example:
void myQueueWarning (boolean watermark) { if(watermark) { // high watermark [ ... ] } else { // low watermark [ ... ] } }
The argument is true when the request count has risen above the high watermark and false when it subsequently drops below the low watermark.
The client interface handles reaching the server limit internally. Once the server limit is reached, calls are queued locally until the high watermark is reached. Therefore, in the preceding example, the application can have a total of up to 2000 calls (3000 - 1000) pending transmission before the client refuses to queue any more.
There are configurable limits for how long a request can be queued before it "times out". Requests can time out at the client before being sent to the server or after being sent. The error responses generated for these cases each have distinct status codes, allowing the application to easily identify each failure condition.
Some invocation attributes defined in the configuration by Client2Config can be overridden on a per-call basis by means of an options object. You create a new option object with Client2CallOptions, then apply it to a procedure call as the first argument. For example, the following code sets a call-specific timeout of 1,234 microseconds and a priority of 3:
Client2CallOptions opts = new Client2CallOptions() .clientTimeout(1234, TimeUnit.MICROSECONDS) .requestPriority(3); addUserCB = callProcedureAsync(opts, "AddUser", username, password);
Options that are not specified in Client2CallOptions are defined by the configuration settings (or their defaults), so the interface is extensible without forcing application code changes.