Application Continuity を Java の UCP(ucp.jar) と Autonomous Database の ATP で試してみる。(Oracle Cloud Infrastructure)

本エントリは JPOUG Advent Calendar 2020 および Oracle Cloud Infrastructure Advent Calendar 2020 の 9日目 の記事です。
以前 sqlplus で検証した Application Continuity を、本記事では Java の UCP(ucp.jar) で試してますやで。
彡(^)(^)

> JPOUG Advent Calendar 2020
> https://adventar.org/calendars/5265
> 
> Oracle Cloud Infrastructure Advent Calendar 2020
> https://qiita.com/advent-calendar/2020/oci
> 
> Application Continuity を sqlplus と Autonomous Database の ATP で試してみる。(Oracle Cloud Infrastructure)
> https://qiita.com/ora_gonsuke777/items/8251cd3079d2904b3934

# 1. Application Continuity とは？

Application Continuity は Oracle Database の高可用性機能の一つで、Oracle Database のトランザクション実行中に
エラーが発生した際に、そのエラーをクライアント側に返すことなく透過的に再実行する機能となります。

> アプリケーション・コンティニュイティ
> https://www.oracle.com/technetwork/jp/database/options/clustering/applicationcontinuity/overview/ac-overview-1967264-ja.html
> 
> oracle4engineer - アプリケーション・コンティニュイティ(Speakerdeck)
> https://speakerdeck.com/oracle4engineer/apurikesiyonkonteiniyuitei

# 2. 参考ドキュメント／マニュアル

以下のマニュアルとソースコードを参考にしてみました。彡(ﾟ)(ﾟ)

> アプリケーション・コンティニュイティの有効化および無効化
> https://docs.oracle.com/cd/E83857_01/paas/atp-cloud/atpug/application-continuity.html#GUID-8874CB1D-0B20-461F-91D2-24E2EE4148A3
> > DBMS_CLOUD_ADMIN.ENABLE_APP_CONTを使用して、選択したサービスでアプリケーション・コンティニュイティを有効にします。
> 
> GitHub: UCPSample.java
> https://github.com/oracle/oracle-db-examples/blob/master/java/jdbc/ConnectionSamples/UCPSample.java
> 
> 10.1 UCPを使用したアプリケーション・コンティニュイティの有効化の概要
> https://docs.oracle.com/cd/F19136_01/jjucp/application-continuity-using-ucp.html#GUID-54F9FD1C-9301-40D3-999B-C74478E72DB0
> > プール対応のデータソースでアプリケーション・コンティニュイティ機能を使用にするには、
> > アプリケーションでoracle.ucp.jdbc.PoolDataSourceインタフェースに対して次のコールを実行する必要があります。
> > // pds is a PoolDataSource
> > pds.setConnectionFactoryClassName("oracle.jdbc.replay.OracleDataSourceImpl");

# 3. 検証環境＆接続トポロジー

以下の環境で検証しています。

> Windows 10(Windows PC) + PowerShell
> Oracle JDK 11.0.9
> Oracle Database 19c (19.8) JDBC Driver & UCP の ojdbc10-full.tar.gz
> Autonomous Databaase の Autonomous Transaction Processing(ATP) 19c

接続トポロジーは以下の通りです。

> Windows PC(PowerShell) => (Internet) => ATP

# 4. JDBC Driver と ATP Wallet の ダウンロード と 解凍、動作確認

以下のページから JDBC Driver と UCP(ucp.jar) を入手します。今回は現時点で最新の 19.8 の ojdbc10-full.tar.gz をダウンロードします。

> JDBC and UCP Downloads page
> https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html

次に ATP の Console から Wallet の zip を ダウンロード します。
![wallet.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/9a3fcd13-7d2b-67b8-e9b0-cd20085b7e5d.jpeg)

ダウンロードした ojdbc10-full.tar.gz と ATP Wallet ZIP を解凍します。今回は Windows PC の下記ディレクトリに解凍しました。

> ojdbc10-full.tar.gz ... C:\tools\ac_java\ojdbc10-full\
> ATP Wallet ZIP ... C:\tools\wallet\Wallet_AYUATP1\

まずは下記記事のプログラムで、動作確認しておく事を推奨します。

> Autonomous DB(ADW/ATP) に Java の JDBC Thin Driver で接続してみる。(OCI, Oracle Cloud Infrastructure)
> https://qiita.com/ora_gonsuke777/items/91ec0e15848a78ede385

Wallet の指定方法に幾つかバリエーションが有るんですが、今回は TNS_ADMIN を JDBC URL を書くパターンを選択彡(ﾟ)(ﾟ)

# 5. テスト用のテーブル作成

ATP に ADMINユーザー で接続して、テスト用のテーブルを作成しておきます。

```sql
CREATE TABLE AC_TEST_TABLE (
    C1 NUMBER
  , C2 VARCHAR2(100)
  , C3 DATE
);
```

# 6. Java + ucp.jar のサンプルプログラム

下記がサンプルプログラムになります。[GitHub](https://github.com/gonsuke777/OracleDatabase/tree/master/ac_java_ucp) にもアップロードしました。
1行 INSERT して1秒ずつスリープする簡単なプログラム……
JDBCドライバクラスの "oracle.jdbc.replay.OracleDataSourceImpl" がポイント彡(ﾟ)(ﾟ)


```java
import java.sql.*;
import oracle.ucp.jdbc.*;
import java.lang.Thread;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class AC_UCPSample {
  final static String DB_URL=   "jdbc:oracle:thin:@ayuatp1_tp?TNS_ADMIN=C:/tools/wallet/Wallet_AYUATP1";
  final static String DB_USER = "ADMIN";
  final static String DB_PASSWORD = "xxx";
  final static String CONN_FACTORY_CLASS_NAME = "oracle.jdbc.replay.OracleDataSourceImpl";

  /*
   * The sample demonstrates UCP as client side connection pool.
   */
  public static void main(String args[]) throws Exception {
    // Get the PoolDataSource for UCP
    PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource();

    // Set the connection factory first before all other properties
    pds.setConnectionFactoryClassName(CONN_FACTORY_CLASS_NAME);
    pds.setURL(DB_URL);
    pds.setUser(DB_USER);
    pds.setPassword(DB_PASSWORD);
    pds.setConnectionPoolName("JDBC_UCP_POOL");

    // Default is 0. Set the initial number of connections to be
    // created when UCP is started.
    pds.setInitialPoolSize(1);

    // Default is 0. Set the minimum number of connections
    // that is maintained by UCP at runtime.
    pds.setMinPoolSize(1);

    // Default is Integer.MAX_VALUE (2147483647). Set the maximum
    // number of connections allowed on the connection pool.
    pds.setMaxPoolSize(1);

    // Default is 30secs. Set the frequency in seconds to enforce
    // the timeout properties. Applies to
    // inactiveConnectionTimeout(int secs),
    // AbandonedConnectionTimeout(secs)&
    //TimeToLiveConnectionTimeout(int secs).
    // Range of valid values is 0 to Integer.MAX_VALUE.
    pds.setTimeoutCheckInterval(10);

    // Default is 0. Set the maximum time, in seconds, that a
    // connection remains available in the connection pool.
    pds.setInactiveConnectionTimeout(10);

    // Set seconds to wait on query.
    pds.setQueryTimeout(300);
    //pds.setFastConnectionFailoverEnabled(true);

    System.out.println("Available connections before checkout: " + pds.getAvailableConnectionsCount());
    System.out.println("Borrowed connections before checkout: " + pds.getBorrowedConnectionsCount());
    // Get the database connection from UCP.
    try (Connection conn = pds.getConnection()) {
      System.out.println("Available connections after checkout: " + pds.getAvailableConnectionsCount());
      System.out.println("Borrowed connections after checkout: " + pds.getBorrowedConnectionsCount());
      // Displays db container name
      GetConainerName(conn);
      // Set Application Continuity test data
      System.out.println("Set AC Data start - " + FormatLocalDateTime(LocalDateTime.now()));
      SetACData(conn);
      System.out.println("Set AC Data end   - " + FormatLocalDateTime(LocalDateTime.now()));
    } catch (SQLException e) {
      e.printStackTrace();
      System.out.println("UCPSample - " + "SQLException occurred : " + e.getMessage());
      throw e;
    }
    System.out.println("Available connections after checkin: " + pds.getAvailableConnectionsCount());
    System.out.println("Borrowed connections after checkin: " + pds.getBorrowedConnectionsCount());
  }
 /*
  * Displays database container name.
  */
  public static void GetConainerName(Connection connection) throws SQLException {
    // Statement and ResultSet are AutoCloseable and closed automatically.
    //final String SQL = "INSERT INTO AC_TEST VALUES(?, ?, SYSDATE)";
    connection.setAutoCommit(false);
    try (Statement statement = connection.createStatement()) {
      try (ResultSet resultSet = statement.executeQuery("SELECT NAME FROM V$CONTAINERS")) {
        while (resultSet.next()) {
          System.out.println("Container Name - " + resultSet.getString(1));
        }
      }
    }
  }
 /*
  * Set Application Continuity test data.
  */
  public static void SetACData(Connection connection) throws SQLException {
    // Insert SQL.
    final String SQL = "INSERT INTO AC_TEST_TABLE VALUES(?, ?, SYSDATE)";
    connection.setAutoCommit(false);
    // Insert 100 rows by waiting 1 second for each row.
    try (PreparedStatement ps = connection.prepareStatement(SQL)) {
      for (int i = 1; i <= 100; i++) {
        System.out.println(i + "..." + FormatLocalDateTime(LocalDateTime.now()));
        ps.setInt(1, i);
        ps.setString(2, "Data" + i);
        ps.executeUpdate();
        try {
          Thread.sleep(1000);
        }
        catch(InterruptedException e) {
          e.printStackTrace();
          throw new RuntimeException("Unexpected interrupt", e);
        }
      }
    }
    connection.commit();
  }
 /*
  * To formatted string yyyy/MM/dd HH:mm:ss.SSS from LocalDateTime.
  */
  public static String FormatLocalDateTime(LocalDateTime ldt) {
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS");
    return ldt.format(dtf);
  }
}
```

# 7. サンプルプログラム動作と ATP の再起動

上記のソースコードを PowerShell でコンパイルして実行してみます。

```java
cd C:\tools\ac_java\
$env:CLASSPATH = "C:\tools\ac_java\ojdbc10-full\ucp.jar;C:\tools\ac_java\ojdbc10-full\ojdbc10.jar;C:\tools\ac_java\ojdbc10-full\ons.jar;C:\tools\ac_java\ojdbc10-full\oraclepki.jar;C:\tools\ac_java\ojdbc10-full\osdt_core.jar;C:\tools\ac_java\ojdbc10-full\osdt_cert.jar;."
javac AC_UCPSample.java
java AC_UCPSample

Available connections before checkout: 0
Borrowed connections before checkout: 0
Available connections after checkout: 0
Borrowed connections after checkout: 1
Container Name - NTNCQ7OHFQU238D_AYUATP1
Set AC Data start - 2020/12/09 10:38:50.743
1...2020/12/09 10:38:50.786
2...2020/12/09 10:38:51.918
:
```

上記のプログラムを動かした状態で ATP を Restart してみると……彡(ﾟ)(ﾟ)
![restart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/5f4917f5-fbfb-403c-1b91-77f2bfce7ad8.jpeg)

```java

:
25...2020/12/09 10:39:17.710
26...2020/12/09 10:39:18.832
java.sql.SQLRecoverableException: ソケットから読み込むデータはこれ以上ありません。
        at oracle.jdbc.driver.T4CMAREngineNIO.prepareForUnmarshall(T4CMAREngineNIO.java:804)
        at oracle.jdbc.driver.T4CMAREngineNIO.unmarshalUB1(T4CMAREngineNIO.java:449)
        at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:410)
        at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:269)
        at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:655)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:270)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:91)
        at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:970)
        at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1205)
        at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3666)
        at oracle.jdbc.driver.T4CPreparedStatement.executeInternal(T4CPreparedStatement.java:1426)
        at oracle.jdbc.driver.OraclePreparedStatement.executeLargeUpdate(OraclePreparedStatement.java:3756)
        at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:3736)
        at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1063)
        at oracle.ucp.jdbc.proxy.oracle$1ucp$1jdbc$1proxy$1oracle$1StatementProxy$2oracle$1jdbc$1internal$1OraclePreparedStatement$$$Proxy.executeUpdate(Unknown Source)
        at AC_UCPSample.SetACData(AC_UCPSample.java:103)
        at AC_UCPSample.main(AC_UCPSample.java:65)
UCPSample - SQLException occurred : ソケットから読み込むデータはこれ以上ありません。
Exception in thread "main" java.sql.SQLRecoverableException: ソケットから読み込むデータはこれ以上ありません。
        at oracle.jdbc.driver.T4CMAREngineNIO.prepareForUnmarshall(T4CMAREngineNIO.java:804)
        at oracle.jdbc.driver.T4CMAREngineNIO.unmarshalUB1(T4CMAREngineNIO.java:449)
        at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:410)
        at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:269)
        at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:655)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:270)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:91)
        at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:970)
        at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1205)
        at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3666)
        at oracle.jdbc.driver.T4CPreparedStatement.executeInternal(T4CPreparedStatement.java:1426)
        at oracle.jdbc.driver.OraclePreparedStatement.executeLargeUpdate(OraclePreparedStatement.java:3756)
        at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:3736)
        at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1063)
        at oracle.ucp.jdbc.proxy.oracle$1ucp$1jdbc$1proxy$1oracle$1StatementProxy$2oracle$1jdbc$1internal$1OraclePreparedStatement$$$Proxy.executeUpdate(Unknown Source)
        at AC_UCPSample.SetACData(AC_UCPSample.java:103)
        at AC_UCPSample.main(AC_UCPSample.java:65)
```

Exception が発生して異常終了します。テーブルの件数も 0件 です。

```sql

SELECT COUNT(*) FROM AC_TEST_TABLE;

  COUNT(*)
----------
         0
```

# 8. Application Continuity の有効化(DBMS_CLOUD_ADMIN.ENABLE_APP_CONT の実行)

Autonomous Database で Application Continuity を有効化するには、サービス名を確認して
そのサービスに対して DBMS_CLOUD_ADMIN.ENABLE_APP_CONTプロシージャ を実行します。
今回は TPサービス に接続しているので、TPサービスに対してプロシージャを実行します。

```sql
SELECT name, drain_timeout FROM v$services;

BEGIN
    DBMS_CLOUD_ADMIN.ENABLE_APP_CONT(
        service_name => 'NTNCQ7OHFQU238D_AYUATP1_tp.adb.oraclecloud.com'
    );
END;
/

SELECT name, drain_timeout FROM v$services;
```

これを実行すると……彡(ﾟ)(ﾟ)

```sql

NAME                                                             DRAIN_TIMEOUT
---------------------------------------------------------------- -------------
NTNCQ7OHFQU238D_AYUATP1_high.adb.oraclecloud.com                             0
NTNCQ7OHFQU238D_AYUATP1_tpurgent.adb.oraclecloud.com                         0
NTNCQ7OHFQU238D_AYUATP1_low.adb.oraclecloud.com                              0
NTNCQ7OHFQU238D_AYUATP1_medium.adb.oraclecloud.com                           0
ntncq7ohfqu238d_ayuatp1                                                      0
NTNCQ7OHFQU238D_AYUATP1_tp.adb.oraclecloud.com                               0

6行が選択されました。 


PL/SQLプロシージャが正常に完了しました。


NAME                                                             DRAIN_TIMEOUT
---------------------------------------------------------------- -------------
NTNCQ7OHFQU238D_AYUATP1_high.adb.oraclecloud.com                             0
NTNCQ7OHFQU238D_AYUATP1_tpurgent.adb.oraclecloud.com                         0
NTNCQ7OHFQU238D_AYUATP1_low.adb.oraclecloud.com                              0
NTNCQ7OHFQU238D_AYUATP1_medium.adb.oraclecloud.com                           0
ntncq7ohfqu238d_ayuatp1                                                      0
NTNCQ7OHFQU238D_AYUATP1_tp.adb.oraclecloud.com                             300

6行が選択されました。
```

DRAIN_TIMEOUT が 0 ⇒ 300 になりました。

# 9. サンプルプログラムの再実行 と ATP再起動

Application Continuity を有効化した状態でサンプルプログラムを実行して、ATP を再起動してみます。
![restart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/5f4917f5-fbfb-403c-1b91-77f2bfce7ad8.jpeg)

```java
java AC_UCPSample

Available connections before checkout: 0
Borrowed connections before checkout: 0
Available connections after checkout: 0
Borrowed connections after checkout: 1
Container Name - NTNCQ7OHFQU238D_AYUATP1
Set AC Data start - 2020/12/09 10:55:40.604
1...2020/12/09 10:55:40.644
2...2020/12/09 10:55:41.794
:
36...2020/12/09 10:56:20.107
37...2020/12/09 10:56:21.234 ★ここで
38...2020/12/09 10:56:58.567 ★中断が発生
39...2020/12/09 10:56:59.699
:
99...2020/12/09 10:58:07.799
100...2020/12/09 10:58:08.929
Set AC Data end   - 2020/12/09 10:58:10.192
Available connections after checkin: 1
Borrowed connections after checkin: 0
```

途中で中断しながらも、プログラムが動作し切ったぞ！彡(^)(^)

```SQL
SELECT COUNT(*) FROM AC_TEST_TABLE;

  COUNT(*)
----------
       100
```

テーブルにデータも挿入されています彡(^)(^)

# 10. まとめ

Java でも UCP(ucp.jar) で Application Continuity や！彡(^)(^)

なお上記サンプルでは40秒弱程度の中断が発生していますが、FAN(Fast Application Notification) と
FCF(Fast Connection Failover) が使える環境なら、瞬断程度で済むはず。余裕が有れば記事書くかも彡(ﾟ)(ﾟ)

# おまけ
UCP(ucp.jar) じゃない、生の JDBC Driver でも Application Continuity できました。
下記ソース参照彡(ﾟ)(ﾟ)
https://github.com/gonsuke777/OracleDatabase/blob/master/ac_java_ucp/AC_Sample.java