Java でタイムアウトなんかの処理を考える、Resilience4J ってライブラリがイケてるらしい

f:id:ts0818:20190711215648p:plain Why is the Doomsday Clock set at 2 minutes to midnight? | IPPNW peace and health blog

thebulletin.org

⇧  「原子力科学者会報」による、「世界終末時計」の時刻、2019年は、残り2分ってことに決まったそうですね、「ウォッチメン(監督:ザック・スナイダー)」は何をやっとるか~!あ、どうも、ボクです。

カップラーメンを作ったとしても、完成する前に世界が終わるんだと...

「止まれ止まれっ!止まれ止まれっ止まれ止まれ止まれ!止まれ止まれ止まれ止まれ止まれ止まれ!止まれ止まれ止まれ止まれ…止まれぇっ…止まれーええぇぇぇえっ!!!!」【アニメ:時をかける少女(紺野真琴)】

時をかける少女botさんのツイート: "「止まれ止まれっ!止まれ止まれっ止まれ止まれ止まれ!止まれ止まれ止まれ止まれ止まれ止まれ!止まれ止まれ止まれ止まれ…止まれぇっ…止まれーええぇぇぇえっ!!!!」(紺野真琴)"


時をかける少女 期間限定スペシャルプライス版 [Blu-ray]

 

というわけで、「時をかける少女(原作:筒井康隆)」のように現実の時間は止められないし、タイムリープで時間を遡ることもできないけれど、それでも、『嵐の中で輝いて その夢をあきらめないで~♪(【アニメ:機動戦士ガンダム 第08MS小隊】『米倉千尋嵐の中で輝いて」』)』ということで、プログラミングの閉じた世界の中では、世界の終末を永遠に来ないようにすることは可能であると。

そんなわけで、今回も Java の話です。

 

Javaタイムアウト なんかを考える

外部と通信するようなシステムの構築になってくると気になるのが、通信が上手くいかなかった時に、処理が止まってしまうことですね。

そんな時にも、システムを稼働させ続ける、それが一番大事~♪ ということで、恋に障害はつきもの、でも恋をしたことないの!という話は置いといて、障害が起こったとしても、システムを止めないで~、っていう設計を、

フォールトトレラント設計(障害許容設計)フォールトトレラントせっけい、Fault tolerant design)は、システム設計の手法であり、システムの一部に問題が生じても全体が機能停止するということなく(たとえ機能を縮小しても)動作し続けるようなシステムを設計するものである。

この用語はハードウェアあるいはソフトウェアの障害があってもほとんど途切れることなく動作し続けるコンピュータシステムの設計を指して使われることが多い。

フォールトトレラント設計 - Wikipedia

⇧  「フォールトトレラント設計」という らしいですぞ。通信が絡んでくるシステムの「タイムアウト」「リトライアウト」なんかもそういう概念ってことで良いのかしら?

 

株式会社リコー 様の記事の画像を流用させていただきました。 

astamuse.com

⇧  上記の図のように、対外システム(この場合、「データ管理サーバ」)なんかへ接続を試みた時に、対外システムからのレスポンス(応答)が いつまで経っても戻ってこない時、Webサーバでは処理待ち状態で、処理が止まってしまうと。

そこで、そういった状態に陥った時に、「タイムアウト」と判断して、次の処理に遷移させようというのが、「タイムアウト」の処理であると。

 

ちなみに、「リトライアウト」は、「タイムアウト」した場合に、何回か通信を試みるものらしいです。

⇧  上記の場合は、タイムアウトの回数とかを、Webサーバで管理して、「リトライアウト」にするかどうかを判断してるようなイメージですかね。

概ね、「リトライ」を3~4回して駄目な場合を、「リトライアウト」とすることが多いようです、シーケンス図を描くのがしんどかったので、上記の図では、1回で「リトライアウト」しちゃってますが。

 

そんな通信の状態を管理するのにうってつけのライブラリが「Resilience4J」というものらしいです。

qiita.com

⇧  上記サイト様が詳しいです。上記サイト様で、「Resilience4J」というライブラリの存在を知りました。

 

By the way、非同期処理ってものが存在するじゃない?

だが、しかし!

非同期処理とかにしたところで、どっちにしろ、ずっと通信の待ち状態で放置しておくわけにもいかないし、それに、いまのところの Java だと非同期処理が、OSに依存してしまうんでした。

taichiw.hatenablog.com

⇧  OSに依存しない非同期処理の実現に向けて、「Project Loom」というプロジェクトが進行中ではあるらしいけど、3年後ぐらいになるのではと言われていますね。

 

なので、これだけは待つけど、っていう時間を決めてしまって、それ以上の時間が経過したタイミングで強制的に、通信を無効とし、次の処理をしてしまおうというのがタイムアウトの考えってことですかね。

というわけで、「Resilience4J」の出番です!

qiita.com

qiita.com

⇧  上記サイト様によりますと、「リングバッファ」というもので管理されてるそうです。

 

「Resilience4J」って?

GitHubに公開されている「Resilience4J」の説明によると、

resilience4j.readme.io

Resilience4j is a lightweight, easy-to-use fault tolerance library inspired by
Netflix Hystrix, but designed for Java 8 and functional programming. Lightweight, because the library only uses Vavr, which does not have any other external library dependencies. Netflix Hystrix, in contrast, has a compile dependency to Archaius which has many more external library dependencies such as Guava and Apache Commons Configuration.

https://resilience4j.readme.io/docs

⇧  「Netfix Hystrix」にインスパイアされた「フォールトトレラント設計」なライブラリで、Java 8 から導入された関数型インターフェイスをバリバリ使ってると。依存関係も「Vavr」ってライブラリだけだから、軽くて速いらしい。 

Resilience4j provides higher-order functions (decorators) to enhance any functional interface, lambda expression or method reference with a Circuit Breaker, Rate Limiter, Retry or Bulkhead. You can stack more than one decorator on any functional interface, lambda expression or method reference. The advantage is that you have the choice to select the decorators you need and nothing else. With Resilience4j you don’t have to go all-in, you can pick what you need. 

https://resilience4j.readme.io/docs

⇧   使いたい機能だけ、必要に応じて導入することができると...う~ん、どれが必要か選ぶのが難しい気がするんだけど...

 

「Resilience4J」を使ってみる

さっそく使ってみますか。

今回は、ビルドツールのGradle で外部ライブラリを管理していこうということで、

resilience4j.readme.io

⇧  Gradle の場合の導入の方法を試しました。

itsakura.com

EclipseでGradleプロジェクトの新規作成は、上記サイト様を参考に 

させていただきましたが、途中、他のサイト様も参考にさせていただきました。

Gradle について、よく分かっていないため、かなりカオスな内容になってしまっていますが、ご了承ください。

では、Eclipseを起動し、

f:id:ts0818:20190715102644p:plain

「ファイル(F)」>「新規(N)」>「Gradle プロジェクト」で。(「Gradle プロジェクト」が表示されてない場合は、「その他(O)...」を選択した先で、「Gradle」>「Gradle プロジェクト」を選択。)

f:id:ts0818:20190715102752p:plain

「プロジェクト名」を適当に入力し、「次へ(N) >」。

f:id:ts0818:20190715102937p:plain

プロジェクトを新規開発するという体で、「ワークスペース設定を上書き」にチェックし、「Gradle バージョンの指定」で。(「Resilience4J」に必要なGradleのバージョンとかの記載が特にないので、選択できる一番新しいバージョンにしちゃいました。)

「次へ(N) >」。

f:id:ts0818:20190715103731p:plain

途中、コンソールに表示されてたんだけど、処理中で選べないけど...

f:id:ts0818:20190715104559p:plain

そして、「プレビュー」が一向に終わらない...

f:id:ts0818:20190715123930p:plain

f:id:ts0818:20190715124330p:plain をクリックで、プレビュー中断しました...Eclipse ブラックボックス過ぎるでしょ...

「完了(F)」で。

f:id:ts0818:20190715104515p:plain

で、Java が使えるように...なって無くない?

f:id:ts0818:20190715105156p:plain

qiita.com

Gradleは init タスクでJava/Groovy/Scalaの雛形プロジェクトを作成することが出来ます。

Gradle初心者によるGradle事始め - Qiita

⇧  「Gradleタスク」の「init」ってのを実行することで、Java プロジェクトができるらしい...

というわけで、「Gradle タスク」の該当するプロジェクトを選択し、「build_setup」>「init」を右クリックし、「Gradle タスクの実行」で。(「Gradle タスク」が表示されてない場合は、「ウィンドウ(W)」>「ビューの表示(V)」>「その他(O)...」を選択し、「Gradle」>「Gradle タスク」で)

f:id:ts0818:20190715110523p:plain

 

で、一向にタスクが終わらない...何で?

etc9.hatenablog.com

⇧  「init」は終わってたらしい...

とりあえず、「init」は停止させます。

f:id:ts0818:20190715112939p:plain

 

理由は分からんのだけれども、どうやら、JAVA_HOME が認識されていなかったので、Javaプロジェクトの雛型が作成されなかったらしい?

qiita.com

qiita.com

⇧  上記サイト様によりますと、

  • プロジェクトのルートに配置した gradle.properties
  • Gradle ホームフォルダ<ユーザのホームフォルダ>\.gradle)に配置した gradle.properties

のいずれかに、JAVA_HOME を設定してあげれば良いようです。

なので、プロジェクトのディレクトリ直下に、「gradle.properties」ファイルを作成で。

f:id:ts0818:20190715130037p:plain

f:id:ts0818:20190715130202p:plain

そしたらば、gradle.properties ファイルができますんで、使用したいJavaまでのパスを設定していきます。

f:id:ts0818:20190715130249p:plain

自分は、「jdk1.8.0_211」を指定しようと思います。

f:id:ts0818:20190715130444p:plain

Gradle で使用するJAVA_HOMEを指定で。 

org.gradle.java.home=[Javaまでのパス]

f:id:ts0818:20190715130753p:plain

 

で、Javaのスケルトン(雛型)を作成するには、

stackoverflow.com

⇧  build.gradle に追記が必要らしい。

ということで、プロジェクトのディレクトリ直下に、「build.gradle」ファイルを作成で。ファイルの内容は、以下のような感じで。

// Apply the java plugin to add support for Java
apply plugin: 'java'
apply plugin: 'eclipse'

task initSourceFolders { // add << before { to prevent executing during configuration phase
   sourceSets*.java.srcDirs*.each { it.mkdirs() }
   sourceSets*.resources.srcDirs*.each { it.mkdirs() }
}    

んで、「Gradle タスク」でプロジェクトのディレクトリの中の、「build_setup」>「init」を実行で。

f:id:ts0818:20190715140607p:plain

f:id:ts0818:20190715131412p:plain

「パッケージエクスプローラー」でプロジェクトのディレクトリを選択した状態で右クリックし「Gradle」>「Gradleプロジェクトのリフレッシュ」で。

f:id:ts0818:20190715131452p:plain

ようやく、見慣れたJavaのWebプロジェクトの雛型のディレクトリ構成になりました。

f:id:ts0818:20190715140842p:plain


 

あらためて、「Resilience4J」を使ってみる

Gradleの情報が錯綜し過ぎてて、準備に時間がかかってしまいました...なんとか「Resilience4J」を導入する準備ができたということで、

で、build.gradle の記述で、超絶な泥沼にハマった(涙)。Gradleの勉強しないと駄目ですな、ネットの情報が錯綜し過ぎてて、どれが本当の情報か分からん...

ちなみに、Gradle 5系から、Gradle 5系以前の記述で非推奨になるものとか出てきたらしい...もうついていけんっすわ...

「build.gradle」に追記します。 

// Apply the java plugin to add support for Java
plugins {
    id 'java'
    id 'eclipse'
}

// 何か知らんけど、これないとJavaのプロジェクトの雛型(スケルトン)ができなかったので
task initSourceFolders { // add << before { to prevent executing during configuration phase
   sourceSets*.java.srcDirs*.each { it.mkdirs() }
   sourceSets*.resources.srcDirs*.each { it.mkdirs() }
}

// ライブラリの取得先リポジトリ
repositories {
      jcenter()
      mavenCentral()
}

// build.gradleのファイル内で使える変数を定義できるらしい
ext {
  versions = [
    'resilience4j' : '0.16.0',
    'junit' : '5.3.1' 
  ] 
}

dependencies {
  // 変数を含む場合は、「"」で囲んでやらないと機能しない、「'」はNG(涙)。
  // resilience4j
  implementation "io.github.resilience4j:resilience4j-circuitbreaker:${versions.resilience4j}"
  implementation "io.github.resilience4j:resilience4j-ratelimiter:${versions.resilience4j}"
  implementation "io.github.resilience4j:resilience4j-retry:${versions.resilience4j}"
  implementation "io.github.resilience4j:resilience4j-bulkhead:${versions.resilience4j}"
  implementation "io.github.resilience4j:resilience4j-cache:${versions.resilience4j}"
  implementation "io.github.resilience4j:resilience4j-timelimiter:${versions.resilience4j}"
  // JUnit 5 系からは、「JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage」ってことで、
  // 「Jupiter」、「Vintage」って2つに分かれてるらしい。後方互換性がるのが「Vintage」らしい。
  testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
  testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
}

んで、あらためて、「Gradle タスク」でプロジェクトのディレクトリの中の、「build_setup」>「init」を実行で。

f:id:ts0818:20190715140607p:plain

ようやく、「Resilience4j」が読み込めました。

f:id:ts0818:20190715162133p:plain

 

そんでは、クラスファイルを作成していきます。

qiita.com

⇧  上記サイト様のコードを写経させていただきました。

クラスは、2つ作成しました。

f:id:ts0818:20190715181954p:plain

ソースコードはこんな感じ。

「/TestResilience4J/src/main/java/external/TimeLimiterUtil.java

package external;

import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import io.vavr.control.Try;

public class TimeLimiterUtil {

  private final ExecutorService threadPool = Executors.newCachedThreadPool();

  // タイムアウトの設定
  private final TimeLimiterConfig config = TimeLimiterConfig.custom()
      .timeoutDuration(Duration.ofMillis(2000))
      .cancelRunningFuture(true)
      .build();

  // 処理を実行
  public String callExternalSystem() {
    Callable callable = TimeLimiter
        .decorateFutureSupplier(
            TimeLimiter.of(config),
            () -> threadPool.submit(() -> innerExternalSystem()));
    return Try.of(callable::call)
        .getOrElse("世界は消えてしまった...");

  }

  // 外部への通信とかの処理
  public String innerExternalSystem() { 
    // 通信したという体で、時間を経過させる
    // 時間の制限
    long timeLimit = 2000; // 世界は救われた
    // long timeLimit = 3000; // 世界は破滅した
    // 処理前の時刻を取得
    long startTime = System.currentTimeMillis();
    long endTime = startTime;
    while (endTime - startTime <= timeLimit) {
      endTime = System.currentTimeMillis();
    }

    return "世界はまだ存在する";
  }
}

「/TestResilience4J/src/main/java/main/TestResilience4J.java

package main;

import external.TimeLimiterUtil;

public class TestResilience4J {

  public static void main(String[] args) {
    // TODO 自動生成されたメソッド・スタブ
    TimeLimiterUtil timeLimiterUtil = new TimeLimiterUtil();
    System.out.println(timeLimiterUtil.callExternalSystem());
    
  }
}

んで、実行してみる。

f:id:ts0818:20190715182529p:plain

f:id:ts0818:20190715182454p:plain

⇧  なんか、結果が出て、コンソールが終了するまでに時間が結構かかるんだけど、スレッドを使ってるのが関係しているのだろうか?

ちなみに、innerExternalSystem() メソッド内のtimeLimit を 3000 にすると、

f:id:ts0818:20190715183015p:plain

まぁ、2秒、3秒って、既に、終末時計の2分とは何の関係もなくなってるけども...そして、せっかくJUnit 5系を導入したのに使ってないや...

というわけで、関数型インターフェイスを勉強せねばですかね...

今回もモヤモヤ感が募った、そんな1日でしたかね...

今回はこのへんで。