20211010のJavaに関する記事は3件です。

Microprofile Fault Toleranceの機能と独自に用意したInterceptor間の実行順序を制御する

環境 Java 8 CDI 2.0 Microprofile Fault Tolerance 2.0 事象 Microprofile Fault Tolerance 2.0 Circuit Breakerのアノテーションと、独自に用意した FooInterceptor を実行するアノテーションを併用した場合、サーキットブレーカが意図せず発動する事象に遭遇しました。 FooInterceptor @Interceptor @Dependent @Foo @Priority(Interceptor.Priority.APPLICATION) public class FooInterceptor { @AroundInvoke public Object invoke(InvocationContext ic) throws Exception { try { return ic.proceed(); } catch (SomeException e) { throw someCondition ? new WrappedException(e) : e; } } } FooInterceptorを呼び出すためのアノテーション @Inherited @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Foo {} サーキットブレーカとFooInterceptorを利用するクラス @ApplicationScoped public class SomeClass { /** * 何かをする * @throws SomeException エラーが発生した * @throws WrappedException someConditionがtrueのときにエラーが発生した * @throws CircuitBreakerOpenException サーキットブレーカ発動中 */ @CircuitBreaker( successThreshold = 1, requestVolumeThreshold = 100, failureRatio = 1, delay = 600_000, failOn = { SomeException.class, }) @Foo public void doSomeThing() { // 何かをする } } この実装では、someConditionがtrueの場合にはサーキットブレーカが発動しないことを期待していました。 SomeClass#doSomeThing にてthrowされた例外がまず FooInterceptor により WrappedException に変換される。WrappedException はfailOn にて定義していないため、サーキットブレーカは発動しないという想定です。しかし実際には、 SomeClass#doSomeThing が一定回数 WrappedException を返したところサーキットブレーカが発動し、その後は CircuitBreakerOpenException を返すようになりました。 解決策 以下の通り、FooInterceptorの優先順位を 5000 とすることで、someConditionがtrueの場合のサーキットブレーカ発動を回避することができました。 独自に用意したInterceptor @Interceptor @Dependent @Foo // @Priority(Interceptor.Priority.APPLICATION) @Priority(5000) public class FooInterceptor { // 省略 } 原因 本事象は、@CircuitBreaker が呼び出すInterceptorのPriorityよりも、 FooInterceptor のPriorityが高かったために発生しました。 実行されるべきInterceptorが複数存在するときの実行順序は、それらが持つPriorityにより決まります。 FooInterceptor のPriorityがより高い場合には、まず FooInterceptor#invoke 、 次に @CircuitBreakerが呼び出すInterceptor#invoke 、最後に SomeClass#doSomething という順序でコールスタックが積まれていくことになります。このとき、 SomeClass#doSomething で発生した例外は @CircuitBreakerが呼び出すInterceptor#invoke が最初に受け取ることになるため、 FooInterceptor#invoke で実施している変換処理が呼ばれる前にサーキットブレーカ発動条件の評価が行われてしまいます。 これを避けるためには、 FooInterceptor のPriorityを、 @CircuitBreaker が呼び出すInterceptorのそれよりも低く設定する必要があります。そうすることで、コールスタックの順序が入れ替わり、FooInterceptor#invoke が SomeClass#doSomething で発生した例外を先にcatchすることができるようになります。では、 @CircuitBreaker のPriorityはいくつに設定されているのでしょうか? Microprofile Fault Tolerance 2.0の仕様では、以下のように記述されており、Microprofile Fault Toleranceの各Interceptorは Priority.PLATFORM_AFTER+10 (4010)から Priority.PLATFORM_AFTER+50 (4050)までの範囲にPriorityを定義するよう定めています。 The base priority of the lowest priority Fault Tolerance interceptor is Priority.PLATFORM_AFTER+10, which is 4010. If more than one Fault Tolerance interceptor is provided by an implementation, the priority number taken by Fault Tolerance interceptor(s) should be in the range of [base, base+40]. FooInterceptor はこれよりもPriorityを低くする、つまりPriorityの値を大きくする必要があります。もともと設定していた Priority.APPLICATION は2000ですので、全然足りていませんでした。4050 よりも大きい値にPriorityを設定することで、事象解消が可能になりました。 おわり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【非同期処理】スレッドセーフって何?

業務で非同期処理を書いたときに「そのクラス、スレッドセーフですか?」と指摘を受けることが複数回ありました。 そこで、「そもそもスレッドセーフって何よ」「スレッドセーフじゃないと何が困るのよ」ということを勉強しました。本記事はそのまとめとなってます。 参考図書 この本は思考させる系の練習問題がついてて、良い気がします。 自分もまだ読破できてませんが(笑) 以下は、この本の第1章「Single Threaded Execution」が基になっています。 「スレッドセーフでない」とは インスタンスまたはメソッドが、複数のスレッドから実行されたときに、期待した挙動をしなくなるような状態のこと。 スレッドセーフじゃないと困る例 要件 ・一度に一人しか通れない門が一つある。通行人が3人いて、それぞれが頻繁にこの門を通る。 ・門が通行人を通るごとに、下記の3つの値を表示したい。  -それまで門を通った人数  -通った人の名前  -通った人の出身地 ・通行人は下記の3人。  -名前:Alice 出身地:Alaska  -名前:Bobby 出身地:Brazil  -名前:Clice 出身地:Canada クラス設計 名前 役割 Main Javaのメインクラス。門を作り、人を通らせる Gate 門を表すクラス。人が通る時に"それまでに通った人数""通った人の名前""通った人の出身地"を表示する UserThread 人を表すクラス。Mainクラスが終了するまで門を通り続ける 実装 ※こちら↓からDL可能です。 ※筆者が作ったものではありません https://www.hyuki.com/dp/dp2.html#download Main.java public class Main { public static void main(String[] args) { System.out.println("Testing Gate, hit CTRL+C to exit."); Gate gate = new Gate(); new UserThread(gate, "Alice", "Alaska").start(); new UserThread(gate, "Bobby", "Brazil").start(); new UserThread(gate, "Chris", "Canada").start(); } } Gate.java public class Gate { private int counter = 0; private String name = "Nobody"; private String address = "Nowhere"; public void pass(String name, String address) { this.counter++; this.name = name; this.address = address; check(); } public String toString() { return "No." + counter + ": " + name + ", " + address; } private void check() { if (name.charAt(0) != address.charAt(0)) { System.out.println("***** BROKEN ***** " + toString()); } } } UserThread.java public class UserThread extends Thread { private final Gate gate; private final String myname; private final String myaddress; public UserThread(Gate gate, String myname, String myaddress) { this.gate = gate; this.myname = myname; this.myaddress = myaddress; } public void run() { System.out.println(myname + " BEGIN"); while (true) { gate.pass(myname, myaddress); } } } Gateクラスでは、checkメソッドで通過する人の名前と出身地の頭文字が異なっていた場合にBROKENを標準出力しています。 動作確認 BROKENがたくさん出ました。なぜでしょう。 しかも、不思議なのは「名前と出身地の頭文字が合っている」「名前と出身地の頭文字が異なる」両方の場合にBROKENが出てしまっていることです。 "複数スレッド(=各UserThreadクラス)から実行されたときに期待した挙動しない"状態なので、ここではGateクラスがスレッドセーフでない状態になっています。 Testing Gate, hit CTRL+C to exit. Alice BEGIN Bobby BEGIN Chris BEGIN ***** BROKEN ***** No.633: Bobby, Brazil //名前と出身地の頭文字が合っている ***** BROKEN ***** No.990: Bobby, Brazil ***** BROKEN ***** No.1511: Alice, Alaska ***** BROKEN ***** No.1928: Alice, Alaska ***** BROKEN ***** No.2512: Bobby, Brazil ***** BROKEN ***** No.2750: Chris, Canada ***** BROKEN ***** No.2998: Bobby, Brazil ***** BROKEN ***** No.3198: Bobby, Brazil ***** BROKEN ***** No.3499: Bobby, Brazil ***** BROKEN ***** No.4024: Bobby, Brazil ***** BROKEN ***** No.2198: Bobby, Brazil ***** BROKEN ***** No.4391: Bobby, Brazil ***** BROKEN ***** No.5290: Alice, Alaska ***** BROKEN ***** No.5514: Chris, Alaska //名前と出身地の頭文字が異なる ***** BROKEN ***** No.4901: Alice, Alaska なぜこうなる? 各UserThreadクラス(=Alice,Bobby,Chris)はpassメソッドを実行し、Gateクラスのクラス変数であるcounter,name,addressを次々と変更します。 Aliceさんは今Gateクラス変数がどのような状態になっているかなどお構いなしにpassメソッドを実行し続けます。BobbyさんもChrisさんも然りです。 もしかしたらタイミングによっては、次のような処理順になることもあります。 BROKEN出力 処理順例1(出力された名前と出身地の頭文字が異なっている場合) AliceさんがGateクラス変数nameをAliceに変更する BobbyさんがGateクラス変数addressをBrazilに変更する Gateクラスのcheckメソッドが実行される nameがAlice,AddressがBrazilなので、name.charAt(0) != address.charAt(0)がtrueとなる BROKEN, Alice, Brazilが出力される BROKEN出力 処理順例2(出力された名前と出身地の頭文字が合っている場合) AliceさんがGateクラス変数nameをAliceに変更する BobbyさんがGateクラス変数addressをBrazilに変更する Gateクラスのcheckメソッドが実行される nameがAlice,AddressがBrazilなので、name.charAt(0) != address.charAt(0)がtrueとなる AliceさんがGateクラス変数addressをAlaskaに変更する BROKEN, Alice, Alaskaが出力される つまり、各スレッド(Alice,Bobby,Chris)がGateクラスに干渉する順番によって、得られる結果が異なる状態になっています。現状、干渉する順は制御できません。 改善方法 Aliceさんは今Gateクラス変数がどのような状態になっているかなどお構いなしにpassメソッドを実行し続けます。BobbyさんもChrisさんも然りです。` ココが良くないので、Gateクラスのメソッド側で「自分を使っていいのは一回に一人だけ、同時に実行しないでね」という制限をかけます。抽象的な言葉で言うと「排他制御」ですね。 具体的にはJavaのsyncronized句を使います。 メソッドにsyncronized句を付けることで、このような仕様が追加されます あるスレッドがsyncronizedメソッドを使う際、メソッドのロックを取得する ロックを取得されたメソッドを他スレッドが使用すると、ブロックされる メソッドの使用が終わるとロックが解放され、他スレッドが使用可能になる (排他制御後)Gate.java public class Gate { private int counter = 0; private String name = "Nobody"; private String address = "Nowhere"; //syncronizedで1度に1スレッドからしか使用できなくする     public synchronized void pass(String name, String address) { this.counter++; this.name = name; this.address = address; check(); } //syncronizedで1度に1スレッドからしか使用できなくする public synchronized String toString() { return "No." + counter + ": " + name + ", " + address; } private void check() { if (name.charAt(0) != address.charAt(0)) { System.out.println("***** BROKEN ***** " + toString()); } } } 動作確認 排他制御前の実行では数秒とまたずにBROKENしていましたが、数秒待ってもBROKENしなくなりました。 (何時間もやってると壊れ始めるのかもしれませんが...) Testing Gate, hit CTRL+C to exit. Alice BEGIN Bobby BEGIN Chris BEGIN どうして治ったの? passメソッドを使えるのが1度に1スレッドになり、下記のような処理順が実現されるためです。 非BROKEN出力 処理順例 Aliceさんがpassメソッドのロックを取得する AliceさんがGateクラス変数nameをAliceに変更する AliceさんがGateクラス変数addressをAlaskaに変更する Aliceさんがpassメソッドのロックを開放する Gateクラスのcheckメソッドが実行される nameがAlice,addressがAlaskaなので、name.charAt(0) != address.charAt(0)がfalseとなりBROKENしない Bobbyさんがpassメソッドのロックを取得する BobbyさんがGateクラス変数nameをBobbyに変更する BobbyさんがGateクラス変数addressをBrazilに変更する Bobbyさんがpassメソッドのロックを開放する Gateクラスのcheckメソッドが実行される nameがBobby,addressがBrazilなので、name.charAt(0) != address.charAt(0)がfalseとなりBROKENしない(以下略) まとめ 「スレッドセーフでない」とは、メソッドやインスタンスが複数スレッドから使用された時に意図しない挙動をしてしまう状態のこと。 原因は、メソッドやインスタンスが複数スレッドで使用される際の使用順が制御されていないため。 防ぐには、排他制御が必要。 排他制御の一つとして、メソッドに「syncronized」句をつける方法がある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fabricのソースコード非公開Modの難読化を解除する方法

はじめに Modを改造したい、そう思ったときにソースコードが公開されていないことはよくあります。 Modをパクリたいとき、改造したいとき、パッチを作りたいとき色々ありますが、 常に難読化の壁に阻まれることになります。 Forgeの難読化を解除する方法はありますが、Fabricの難読化を解除する方法についての情報がなかったので記事を書きます。 1. 前提知識 2. Modを準備する デコンパイルするFabricで製作されたModを用意します。 今回は例として OAuth-Fabric を使用します。 (GitHubにソースコードが配布されているためデコンパイルする必要ないですが説明のため使用します) 今回はC:\example\oauth-fabric-1.0.jarに配置しました。 3. tiny-remapperを準備する tiny-remapperはFabric Loom(FabricのMod環境を構築するツール)に使われている難読化解除ツールです。 Fabricのmavenリポジトリよりダウンロードできます。 -fatとついているjarファイルをダウンロードしてください。(必要なライブラリがすべて含まれています) 今回はtiny-remapper-0.6.0-fat.jarをダウンロードしました。 今回はC:\example\tiny-remapper-0.6.0-fat.jarに配置しました。 4. 難読化マッピング名を確認する 使用しているFabric Mod開発環境のマッピングを確認します。 build.gradleの中にマッピングが書かれている場所があります。 今回は1.17.1+build.61であることが分かりました。 build.gradle dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" // ← ここがマッピング名です。gradle.propertiesを参照しているようです。 // ~~~ } gradle.properties minecraft_version=1.17.1 yarn_mappings=1.17.1+build.61 # ↑ ありました。これがマッピング名です。 5. 難読化マッピングを準備する Gradleのキャッシュディレクトリにtinyファイルがあります。 今回の1.17.1+build.61用のtinyファイルは以下の場所にありました。 C:\Users\<名前>\.gradle\caches\fabric-loom\1.17.1\net.fabricmc.yarn.1_17_1.1.17.1+build.61-v2 今回はC:\example\mappings.tinyに配置しました。 6. Fabric用のマインクラフトのjarを入手する これがなくても一部の場所は書き換わってくれるのですが、オーバーライドした関数名など一部がそのままになってしまいます。 クラスパスを適切に指定してあげることでオーバーライドした関数名なども適切に難読化解除することができます。 Gradleのキャッシュディレクトリにminecraft-intermediary.jarファイルがあります。 今回の1.17.1+build.61用のminecraft-intermediary.jarファイルは以下の場所にありました。 C:\Users\<名前>\.gradle\caches\fabric-loom\1.17.1\net.fabricmc.yarn.1_17_1.1.17.1+build.61-v2 今回はC:\example\minecraft-intermediary.jarに配置しました。 7. 難読化解除する 必要なファイルが整いました。 必要なファイルは以下の3つです。 C:\example>tree C:\example /f C:\EXAMPLE mappings.tiny ← 手順5で用意したマッピングファイル minecraft-intermediary.jar ← 手順6で用意したマインクラフトのファイル oauth-fabric-1.0.jar ← 手順2で用意したModファイル tiny-remapper-0.6.0-fat.jar ← 手順3で用意したtiny-remapper ファイルがあるディレクトリでコマンドプロンプトを開き以下のコマンドを打ちます。 C:\example>java -jar tiny-remapper-0.6.0-fat.jar oauth-fabric-1.0.jar oauth-fabric-1.0.dev.jar mappings.tiny intermediary named minecraft-intermediary.jar Finished after 645.38 ms. 難読化解除されたjarファイル oauth-fabric-1.0.dev.jar が生成されます。 これをIntellij IDEAや、JD-GUIで開くことでソースコードを読むことができるようになります。 お疲れさまでした。 補足: tiny-remapperのコマンドについて 先程のコマンドは java -jar tiny-remapper-0.6.0-fat.jar oauth-fabric-1.0.jar oauth-fabric-1.0.dev.jar mappings.tiny intermediary named java -jar tiny-remapper-0.6.0-fat.jar <入力ファイル名.jar> <出力ファイル名.jar> <マッピングファイル.tiny> <変換前マッピングタイプ> <変換後マッピングタイプ> [クラスパス]... java -jar tiny-remapper-0.6.0-fat.jar でヘルプが見れます。追加オプションがいくつかあります。 変換マッピングタイプはintermediary(難読化状態)とnamed(解除状態)があります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む