インメモリDB　VoltDBでプロシージャコール時のクライアント側タイムアウトを制御する

# VoltDBでプロシージャコール時のクライアント側タイムアウトを制御する

VoltDBのクライアント側で処理すべきエラーとして、プロシージャのタイムアウトと接続タイムアウトがあります。
プロシージャのタイムアウトと接続タイムアウトはデフォルトで2分間(120秒)になっています。
タイムアウトが発生すると、同期プロシージャコールではCONNECTION_TIMEOUTエラーが返り、
非同期プロシージャの場合は、clientResponseオブジェクトのエラー情報を含むコールバックが呼び出されます。

このタイムアウト時間を変更する場合、ClientConfigオブジェクトのメソッドsetProcedureCallTimeoutおよびsetConnectionResponseTimeoutメソッドを使用します。
このメソッドの引数に"0"を設定した場合はタイムアウトが無効になります。
例えば、次のコードはプロシージャタイムアウトを10秒、接続タイムアウトを60秒に設定しています。

```java
			config = new ClientConfig();
			config.setProcedureCallTimeout(10 * 1000); // プロシージャコールのタイムアウトを10秒に設定
			config.setConnectionResponseTimeout(60 * 1000); // 接続タイムアウトを60秒に設定

			client = ClientFactory.createClient(config);
```

タイムアウトを設定した際のmain関数全体は以下のようになります。
GetDataFromTestAというテスト用のプロシージャを実行しており、このプロシージャは事前に作成しています。プロシージャの作成方法は後で説明します。

```java
	public static void main(String[] args) {

		Client client = null;
		ClientConfig config = null;

		try {
			config = new ClientConfig();
			config.setProcedureCallTimeout(10 * 1000); // プロシージャコールのタイムアウトを10秒に設定
			config.setConnectionResponseTimeout(60 * 1000); // 接続タイムアウトを60秒に設定

			client = ClientFactory.createClient(config);
			client.createConnection("192.168.10.171");

	    	VoltTable[] results;
	    	ClientResponse res = client.callProcedure("GetDataFromTestA", 1);

	    	if (res.getStatus() == ClientResponse.SUCCESS) {
	    		System.out.println("SELECTに成功");
	    	}
		} catch (ProcCallException e) {
			ClientResponse res = e.getClientResponse();
			if (res.getStatus() == ClientResponse.CONNECTION_TIMEOUT) {
				System.out.println("CONNECTION_TIMEOUT発生");
			} else if (res.getStatus() == ClientResponse.CONNECTION_LOST) {
				System.out.println("CONNECTION_LOST発生");
			}
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
```

実際にプロシージャタイムアウトになるプロシージャ（GetDataFromTestA）を実行してみます。
プロシージャタイムアウトを10秒間に設定したので、プロシージャ（GetDataFromTestA）は20秒のスリープを入れました。

実行するとタイムアウトに設定した10秒経過後に以下のログが出力され、タイムアウトを検出していることが確認できます。

```
CONNECTION_TIMEOUT発生
org.voltdb.client.ProcCallException: No response received in the allotted time (set to 10000 ms).
	at org.voltdb.client.ClientImpl.internalSyncCallProcedure(ClientImpl.java:480)
	at org.voltdb.client.ClientImpl.callProcedureWithClientTimeout(ClientImpl.java:319)
	at org.voltdb.client.ClientImpl.callProcedure(ClientImpl.java:255)
	at sample.TimeoutTest.main(TimeoutTest.java:30)

```


# エラー用のリスナー設定

プロシージャコールがタイムアウトしていても、実際にはサーバでのプロシージャの実行は完了し、タイムアウト後にレスポンスが返ってくるケースの方が多いです。
その際にクライアント側でレスポンスを受け取るようにリスナーを設定することができます。
リスナーはClientStatusListenerExtクラスを継承し、以下のように作成します。
作成したコードはログを出力しているだけですが、実際にはケースごとに適切な処理を実行する必要があります。
コードは以下の公式サイトを流用しています。

[Using VoltDB - 6.5.3. Writing a Status Listener to Interpret Other Errors](https://docs.voltdb.com/UsingVoltDB/DesignAppErrHandling.php#DesignAppErrOther)


```java
	@Override
	public void lateProcedureResponse(ClientResponse response,
			String hostname, int port) {
		System.out.printf("プロシージャタイムアウト発生後にレスポンス受信　ホスト： %s:%d\n", hostname, port);

		if (response.getStatus() == ClientResponse.SUCCESS) {
    		System.out.println("lateProcedureResponse： プロシージャ実行に成功");
    	}
	}
```

lateProcedureResponseメソッドはプロシージャタイムアウト後にレスポンスを受信します。ClientResponseでプロシージャの実行結果が入っているので、そのステータスを"response.getStatus() == ClientResponse.SUCCESS"で確認しています。

リスナー全体のソースは以下のようになります。
connectionLost、backpressure、uncaughtExceptionメソッドの説明は、上述のURLを参照してください。

```java
package sample;

import org.voltdb.client.ClientResponse;
import org.voltdb.client.ClientStatusListenerExt;
import org.voltdb.client.ProcedureCallback;

public class ClientStatusListenerImpl extends ClientStatusListenerExt {

	@Override
	public void lateProcedureResponse(ClientResponse response,
			String hostname, int port) {
		System.out.printf("プロシージャタイムアウト発生後にレスポンス受信　ホスト： %s:%d\n", hostname, port);

		if (response.getStatus() == ClientResponse.SUCCESS) {
    		System.out.println("lateProcedureResponse： プロシージャ実行に成功");
    	}
	}

	@Override
	public void connectionLost(String hostname, int port,
			int connectionsLeft,
			DisconnectCause cause) {
		System.out.printf("A connection to the database has been lost."
				+ "There are %d connections remaining.\n", connectionsLeft);
	}

	@Override
	public void backpressure(boolean status) {
		System.out.println("Backpressure from the database "
				+ "is causing a delay in processing requests.");
	}

	@Override
	public void uncaughtException(ProcedureCallback callback,
			ClientResponse r, Throwable e) {
		System.out.println("An error has occurred in a callback "
				+ "procedure. Check the following stack trace for details.");
		e.printStackTrace();
	}

}
```

main関数は以下のようになります。


```java
	public static void main(String[] args) throws InterruptedException {

		Client client = null;
		ClientConfig config = null;
		ClientStatusListenerImpl listener = null;

		try {
			listener = new ClientStatusListenerImpl();

			config = new ClientConfig("", "", listener);
			config.setProcedureCallTimeout(10 * 1000);
			config.setConnectionResponseTimeout(60 * 1000);

			client = ClientFactory.createClient(config);
			client.createConnection("192.168.10.171");

	    	VoltTable[] results;
	    	ClientResponse res = client.callProcedure("GetDataFromTestA", 1);

	    	if (res.getStatus() == ClientResponse.SUCCESS) {
	    		System.out.println("SELECTに成功");
	    	}
		} catch (ProcCallException e) {
			e.printStackTrace();
			ClientResponse res = e.getClientResponse();
			if (res.getStatus() == ClientResponse.CONNECTION_TIMEOUT) {
				System.out.println("CONNECTION_TIMEOUT発生");
			} else if (res.getStatus() == ClientResponse.CONNECTION_LOST) {
				System.out.println("CONNECTION_LOST発生");
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			Thread.sleep(60000);
		}
	}
```

リスナーに関するコードは以下の箇所になります。
ClientConfigのコンストラクタの第一引数と第二引数は認証用のユーザIDとパスワードです。
デフォルトでは認証なしになっているので、""を指定しています。
3番目の引数に作成したlistenerのインスタンスを指定します。

```java
		ClientStatusListenerImpl listener = null;

		try {
			listener = new ClientStatusListenerImpl();

			config = new ClientConfig("", "", listener);
```

プロシージャは前述のとおり20秒のスリープを入れていますので、実行から20秒後にタイムアウト後にレスポンスを受信した以下のログが出力されました。

```
プロシージャタイムアウト発生後にレスポンス受信　ホスト： 192.168.10.171:21212
lateProcedureResponse： プロシージャ実行に成功
```

クライアント側ではタイムアウト10秒に設定していたためタイムアウトしていますが、その後にプロシージャが成功したことが確認できました。

最後に、実行時間が長いプロシージャの作成手順を説明します。


# 実行時間が長いプロシージャを作成する

今回の実行時間が長いプロシージャの作成方法を説明します。
まずはテスト用のテーブルを作成します。

```console
$ sqlcmd
SQL Command :: localhost:21212
1> CREATE TABLE TEST_A (
  2>    ID varchar(50) NOT NULL,
  3>    DATA smallint,
  4>    PRIMARY KEY (ID)
  5> );
Command succeeded.
6> PARTITION TABLE TEST_A ON COLUMN ID;
Command succeeded.
```

ストアドプロシージャを以下のように作成します。
実行に時間がかかるプロシージャを作るのは難しいため、実行時に20秒間スリープを入れるようにしています。

```java
package test;

import org.voltdb.SQLStmt;
import org.voltdb.VoltProcedure;
import org.voltdb.VoltTable;

public class GetDataFromTestA extends VoltProcedure {

	public final SQLStmt selectData = new SQLStmt("select data from TEST_A where id = ?");

	public VoltTable run(String name) {

		voltQueueSQL(selectData, name);
		VoltTable[] results = voltExecuteSQL();
		
		try {
			Thread.sleep(20000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		return results[0];
	}
}
```

コンパイルし、jarファイルに固めた後、VoltDBのサーバにjarファイルを格納します。

```console
1> load classes catalog.jar;
Command succeeded.
2> CREATE PROCEDURE FROM CLASS test.GetDataFromTestA;
Command succeeded.

3> show classes;

--- Active Procedure Classes ---------------------------------
 test.GetDataFromTestA     
```

実行すると、"Returned 0 rows in 20.02s"と出力され、実行に20秒以上かかっていることが分かります。これで作成は完了です。

```console
4> exec GetDataFromTestA 1;
DATA 
-----

(Returned 0 rows in 20.02s)
```

# プロシージャコール毎にタイムアウト値を設定する

これまでのタイムアウトの設定はクライアントで共通でした。
callProcedureWithTimeoutメソッドを使用すると、プロシージャコール毎にタイムアウト値を設定することができます。
このcallProcedureWithTimeoutメソッドは以下のリリースノートに記載されているようにv5.7で追加されています。

- [Release notes - 7.7.Ability to set a query timeout for each query](https://docs.voltdb.com/v5docs/ReleaseNotes/)

また、次のJavaDocにcallProcedureWithTimeoutメソッドの説明があります。

- https://docs.voltdb.com/javadoc/java-client-api/org/voltdb/client/Client.html

実際にcallProcedureWithTimeoutメソッドを使用した例は以下のようになります。
callProcedureWithTimeoutメソッドの第一引数にミリ秒（ms）でタイムアウト値を設定します。

```java
ClientResponse res = client.callProcedureWithTimeout(5000, "GetDataFromTestA", 1);
```

ただし、以下のソースを見る限り、（たぶん）v8.3ではバグがあり、タイムアウト値を設定してもデフォルトの2分が使用されてしまいます。それが直ったとしても単位がずれている気がする。。。（ms -> s）

https://github.com/VoltDB/voltdb/blob/master/src/frontend/org/voltdb/client/ClientImpl.java


# 参考

[Using VoltDB - 6.5.2. Handling Timeouts](https://docs.voltdb.com/UsingVoltDB/DesignAppErrHandling.php#DesignAppErrTimeout)

[Using VoltDB - 6.5.3. Writing a Status Listener to Interpret Other Errors](https://docs.voltdb.com/UsingVoltDB/DesignAppErrHandling.php#DesignAppErrOther)

[JavaDoc Client](https://docs.voltdb.com/javadoc/java-client-api/org/voltdb/client/Client.html)